TAMUctf 2019 — write-ups (part 1)

Mar 15 · 15 min read

This CTF took place from Feb. 23rd 2019, 00:00 UTC to March 4th 2019, 00:00 UTC and was organized by Texas A&M University. It was a really accessible but challenging CTF due to the number of teams participating (some pro) and the 9 days duration. The TAMUctf team did a great job! Even if geared more towards university students, some categories were really not common in CTFs, like the PENTESTS, SECURE CODING or the SCENARIO based challenges.

I played alone and was able to validate 61 of the 70 challenges. This led me to the 42nd position over 1769 scoring teams.

validated challenges

Due to the number of challenges, these write-ups will be split into multiple parts. This first part will be dedicated to the categories: WEB, SECURE CODING, NETWORK/PENTEST and ANDROID.

You can read Part 2 for the PWN, REVERSING, CRYPTO and MISC categories. And Part 3 for the HONEYPOT and the scenario-based DRIVE BY INC, READING RAINBOW and MICRO SERVICES challenges.

Note 1: Technically, I found the solution of a 62nd challenge (ReversingErirefvat) a few minutes before the end, however, I could not validate it because the flag format expected was not specified (…and I was to tired to even think using the CTF’s flag format). Its solution will be included in this post series.

Note 2: unless otherwise stated, all commands and scripts you will find below are run on macOSX. Especially sed and base64 syntax may slighly differ from Linux versions. Python2 is the preferred interpreter.

[WEB — 100] Not Another SQLi Challenge (solved: 1184)


Not Another SQLi Challenge — landing page

As the title mentions, there is a SQL injection to exploit here. The POST call done when login in is username=aaa&password=bbb. We can bypass the authentication with username=admin%27%23&password= (admin'# when url-decoded). We get the flag once authenticated as admin.

Flag: gigem{f4rm3r5_f4rm3r5_w3'r3_4ll_r16h7}

[WEB — 100] Robots Rule (solved: 776)


As the challenge title indicates, this challenge points us to the robots.txt file that can be used to give directives to search engine crawlers. For more information about it check this site.

Let’s check the robots.txt file of this website:

Robots Rule — robots.txt — ‘human’ user-agent

Nothing really useful when we browse the file ‘as a human’. What if we do it ‘as a robot’. We just need to change our user-agent to Googlebot, for instance, to get the flag:

Robots Rule — robots.txt — ‘robot’ user-agent

Flag: gigem{be3p-bOop_rob0tz_4-lyfe}

[WEB — 316] Many Gig’ems to you! (solved: 510)


The flag for this challenge is hidden in different parts of the website. There is no vulnerability to exploit.

The first part is hidden in the source code of the index.html page:

Many Gig’ems to you! — index.html source code

The second part is hidden in the source code of the cookie.html page:

Many Gig’ems to you! — cookies.html source code

And the third part is in a cookie created when browsing cookie.html which executes cook.js:

Many Gig’ems to you! — cook.js

Flag: gigem{flag_in_source_and_cookies}

[WEB — 325] Science! (solved: 498)


As soon as we browse the website, we see that Flask may be in use. Flask is a Python framework that uses Jinja2 templating. Therefore, the first thing we will try is to exploit a Server-Side Template Injection (SSTI). This good article from PortSwigger may help to better understand what an SSTI is. To exploit a Jinja2 SSTI, one can read this post.

Science! landing page

We have 2 inputs we can play with. Jinja2 uses double curly-braces to evaluate an expression, variable or function call and print the result into the template.

If we input 1 and 2 respectively for chemical 1 and 2, those values are output as is:

Science1 — normal output

If we inject an expression inside double curly-braces, we see it gets evaluated:

Science! — expression evaluation

We can read the current application configuration with {{config.items()}} but we do not get anything interesting from it. As Jinja2 uses Python, we can use built-in attributes and introspection to find a way to read files and execute commands.

To start, we can get the list of all available classes with {{().__class__.__base__.__subclasses__()}}. The list is pretty long but we see we have access to the class warnings.catch_warnings which is at index 59 (this may change depending on the environment):

Science! — list of available classes

This is a well-known class in CTFs and it is used in many Python jail escape challenges as well. Indeed, this class has a _module attribute which contains the attribute __builtins__ with which we can call the __import__ function and load any existing module. In our case we want to run OS commands, therefore, we load the os module to run commands as follows:

Science! — command injection 1

Now that we have listed the web server local files, we can read the flag.txt file:

Science! — command injection 2

Flag: gigem{5h3_bl1nd3d_m3_w17h_5c13nc3}

[WEB — 362] Buckets (solved: 443)

Checkout my s3 bucket website!

We can use S3Scanner to scan open Amazon S3 buckets. We only need to add our site tamuctf to the sites.txt file and run the scanner:

S3Scanner output

The content of the S3 bucket can be found in the ./buckets/tamuctf folder. The flag can be found in ./buckets/tamuctf/Dogs/CC2B70BD238F48BE29D8F0D42B170127/CBD2DD691D3DB1EBF96B283BDC8FD9A1/flag.txt


[WEB — 473] Login App (solved: 198)


We are welcomed with a login box and no much more information. Trying some basic SQL injections does not show any errors or any specific behavior.

Login App — landing page

By looking a the HTTP headers we see that the website is powered by Express which is a back-end framework running on top of Node.js. This is some interesting information as Node.js and Express are often coupled with MongoDB and Angular in what is known as the MEAN stack (although Angular is not used here). As MongoDB is a NoSQL database, we can try some NoSQL injections instead of the usual SQL injections.

When login in, the POST data is a JSON payload like {"username":"admin", "password":"1234"} on /login . A simple authentication bypass in NoSQL could be:

{ "username": "admin", "password": {"$ne": null} }

This meand that if the user admin exists and has a password not equal to null, the above query will be True. We get the flag using the above query with curl:

Login App — authentication bypass

Flag: gigem{n0_sql?_n0_pr0bl3m_8a8651c31f16f5dea}

[WEB — 478] Bird Box Challenge (solved: 177)

We’ve got Aggies, Trucks, and Eggs!

The webpage only feature is a search form and only seems to respond to Aggies, Trucks and Eggs terms showing the corresponding image.

Bird Box — landing page

The challenge title, Bird Box, is a Netflix movie that “follows a woman and a pair of children, who must travel through a forest and down a river blindfolded to avoid supernatural entities that cause people who look at them to commit suicide”. This looks like a hint that the vulnerability could be a Blind SQL Injection.

Trying some basic queries we see that:

  • we can exploit a boolean SQL injection:
http://web2.tamuctf.com/Search.php?Search=Eggs’ and 1=1-- - // true
http://web2.tamuctf.com/Search.php?Search=Eggs’ and 1=2-- - // false
  • spaces are filtered in union based SQL injections,
  • using sqlmap does not seem to work as the HTTP response truncates the image and it can’t differentiate true from false queries.

At the time of the CTF, I only explored the boolean blind SQL injection. I re-used this script I did for another CTF to retrieve the table’s information:

Bird Box — db dump

The flag is the username of the current database user.

Flag: gigem{w3_4r3_th3_4ggi3s}

Note: I spent some time on the union SQLi and sqlmap issue after the end of the CTF. In fact, bypassing the Union SQLi filter was really easy. Using '/**/union/**/select/**/user()-- - gives us the flag directly. Regarding sqlmap, it appears that there is a filter on sqlmap’s user agent, therefore, adding the --random-agent flag resolves the issue.

[WEB — 485] 1337 Secur1ty (solved: 148)


We start by registering an account:

1337 Secur1ty — registration

And use the QR-Code to add the account to Google Authenticator application as requested:

1337 Secur1ty — QR-code

Once logged in, we have access to the list of employees registered:

1337 Secur1ty — employees

And the list of messages that users sent to us:

1337 Secur1ty — messages

By clicking on the message id, we can see the message details. The GET request is /message?id=2. By selecting id=1 we can see a message sent by Bob to the admin and that says:

Please don’t blow off the meeting today, we need to talk about the cookies.

The cookies set upon registering are:


However, cookies are not really useful here as the message id is vulnerable to SQL injections (at least boolean & union based) and we can use sqlmap to dump the full database. The message feature is accessible unauthenticated:

$ sqlmap -u http://web6.tamuctf.com/message?id=1 --technique=U --dump
1337 Secur1ty — sqlmap output

We change our cookies to use the admin secret and id and get access to his account. We can read the flag in the profile details.

Flag: gigem{th3_T0tp_1s_we4k_w1tH_yoU}

[SECURE CODING — 419] SQL (solved: 340)


This challenge is linked to Not Another SQLi Challenge web challenge. We now need to patch the vulnerability by committing the patch using GitLab.

We first need to fork the tamuctf GitLab project called SQL. Then we can clone it and start to work on it:

$ git clone https://gitlab.tamuctf.com/noobintheshell/sql.git

By reading login.php we see 2 main things to patch:

  • error management (line 2 and 3) => disable error display,
  • user inputs are not validated which results in SQL injection (line 13–15) => use prepared statements.

Once the code is patched, we commit it as follows:

$ git add login.php
$ git commit -m 'SQL injection patch'
$ git push origin master

Once the commit is done, the pipeline configured in .gitlab-ci.yml is triggered. Once the job ends, if the patch is correct, the flag is shown in the job details.

Here are the details of the patch:

login.php diff

The original and patched file can be found here.

Flag: gigem{the_best_damn_sql_anywhere}

[SECURE CODING — 423] PWN (solved: 332)


The goal is the same as the previous challenge. This time we need to patch some basic C code containing a buffer overflow. Indeed, the gets function is used and the size of the buffer is not checked. It is recommended to use fgets instead which includes the buffer size as argument.

vuln.c diff

The original and patched file can be found here.

Flag: gigem{check_that_buffer_size_baby}

[SECURE CODING — 455] Science (solved: 253)


This one is related to the Science! web challenge. We need to patch the Server-Side Template Injection. Note that we can as well inject JS and need therefore to escape characters. To validate the user input, we can simply strip the curly braces and escape the string. Moreover, we can disable the debug mode in prod.

serve.py and views.py diff

The original and patched files can be found here.

Flag: gigem{br0k3n_fl4sk_2d88bb862569}

[SECURE CODING — 487] Login App2 (solved: 141)


Here we need to patch the NoSQL injection found in the Login App web challenge. For this patch, I re-used what mongo-sanitize module does.

server.js diff

The original and patched file can be found here.

Flag: gigem{3y3_SQL_n0w_6b95d3035a3755a}

[NETWORK/PENTEST — 324] Stop and Listen (solved: 499)

Sometimes you just need to stop and listen.

This challenge is an introduction to our network exploit challenges, which are hosted over OpenVPN.

The standard subnet is, so give that a scan ;)

We are given an OpenVPN configuration file to connect to the challenge. I used Tunnelblick on macOS. Once the VPN tunnel set, we get the address on the tap0 interface.

As the title suggests, we fire Wireshark to listen on the tap0 interface. We receive 58 UDP packets on port 5005 from The last packet contains the flag in clear:

Stop and Listen — Wireshark

Flag: gigem{f0rty_tw0_c9d950b61ea83}

[NETWORK/PENTEST — 469] Wordpress (solved: 213)

I setup my own Wordpress site!
I love that there are so many plugins. My favorite is Revolution Slider. Even though it’s a little old it doesn’t show up on wpscan!

Please give it about 30 seconds after connecting for everything to setup correctly.
The flag is in /root/flag.txt

We have access to a Wordpress website on that we can scan with WPScan:

$ wpscan --url
WPScan output snippet

The first vulnerability can be used to download a local file, for instance:

The second one is more interesting as there is a Metasploit plugin to exploit it.

meterpreter shell

We can use the meterpreter command shell to browse the host more easily. Reading /var/www/wp-config.php we get the database configuration:

define(‘DB_NAME’, ‘wordpress’);
define(‘DB_USER’, ‘wordpress’);
define(‘DB_PASSWORD’, ‘0NYa6PBH52y86C’);
define(‘DB_HOST’, ‘’);

We see that the database runs on another host: Moreover, we see as well a note.txt file in the website root folder that contains the following message:

Your ssh key was placed in /backup/id_rsa on the DB server.

Let’s connect to this database as we have the credentials for the user wordpress:

$ mysql -u wordpress -p wordpress -h

We could add a new admin user to have persistence on the Wordpress website, however, we only need to read /backup/id_rsa and this can be done with load_file() function:

mysql> select load_file(“/backup/id_rsa”);

We copy the private key locally and connect to the web server as root to get the flag (don’t forget to set the private key permissions to 400 or 600):

SSH root access

Flag: gigem{w0rd_pr3ss_b3st_pr3ss_409186FC8E2A45FE}

[NETWORK/PENTEST — 474] Calculator (solved: 193)

Using a teletype network protocol from the 70s to access a calculator from the 70s? Far out!

Note to new players: You won’t see anything in Wireshark / tcpdump when you initially connect. (i.e. packets are sent unicast on a bridged network)

By scanning the provided range for existing hosts, we see 2 hosts:

  • with telnet port 23 open,

Except for some broadcasted traffic, we do not get anything using Wireshark. Let’s use Ettercap to do an ARP poisoning attack and redirect the traffic of those 2 hosts through us:

$ sudo ettercap -i tap0 -T -M ARP / /

We start to see the traffic between the 2 hosts in Wireshark. connects regularly to with Telnet. As Telnet sends the credentials in clear we can easily retrieve them:

Calculator — telnet sniffing

We can, therefore, telnet to alice:58318008 and get the flag:

Calculator — telnet connection

Flag: gigem{f5ae5f528ed5a9ad312f75bd1d3406a2}

[ANDROID — 376] Secrets (solved: 420)

Can you find my secrets?

We get an Android binary howdyapp.apk to analyze. We decompile the binary with jadx:

$ jadx -d out howdyapp.apk

Then a basic string search of the output is sufficient to retrieve the flag which is base64 encoded in the strings resource file:

howdyapp.apk — source code string search
$ echo “Z2lnZW17aW5maW5pdGVfZ2lnZW1zfQ==” | base64 -D

Flag: gigem{infinite_gigems}

[ANDROID — 460] Local News (solved: 240)

Be sure to check your local news broadcast for the latest updates!

We get a new Android binary app.apk. We decompile it again with jadx (one can as well use the jadx-gui instead).

The MainActivity.java shows that the app instantiates a Broadcast Receiver and logs something when a com.tamu.ctf.hidden.START intent is received:

com/tamu/ctf/hidden/MainActivity.java snippet

The logged string has 2 parts. The first value can be found in the strings resources:

R.string.flag = 'Flag:'

The second part uses a custom class Deobfuscator$app$Debug.java to retrieve the flag value:


We do not need to understand the code though. We can copy/paste the class, add the following code in the class and run it to get the flag (source code):

public static void main(String[] args){

Flag: gigem{hidden_81aeb013bea}

[ANDROID — 499] Howdy Keystore (solved: 47)

One of your senior design team members decided to drop out of the university at the last minute to pursue a life of gigging. Before leaving they never gave the team the password to the keystore file for the Android app y’all were making. Can you recover the password? You seem to recall that they were a good Aggie despite dropping out.

The flag is in gigem{password} format.

The password contains Uppercase, lowercase, and numbers only.

Hint: https://en.wikipedia.org/wiki/Glossary_of_Texas_A%26M_University_terms

We have a Java keystore to brute-force and we have a hint that the password could be made of terms related to Texas A&M University (TAMU).

The first thing to do is to extract the password hash form the keystore. This can be done with JKS private key cracker that extracts it in a Hashcat format:

$ java -jar JksPrivkPrepare.jar howdyapp.keystore > hash.txt

Next step is to create a custom wordlist based on TAMU terms using the link of the hint. We can retrieve the main words in small letters. We will then use RSMangler to combine words, mix case, add prefixes and suffixes, etc. to create a more complete word list.

You can find the initial wordlist I used here. The wordlist contains 18 words. We want to expand this list with words combinations and add number prefixes and suffixes

Note: I initially tried to use tools like crunch or RSMangler to create words permutations and manipulations. However, they struggle to handle wordlists of more than 6–7 words as it is not possible to limit the number of words to use for combinations (…or I was not able to find how).

I used the following script to add combinations of up to 3 words of the wordlist and add numbers from 0 to 9999 as prefix and suffix. To keep the wordlist smaller, we will use Hashcat rules to toggle letter’s case as we know there are capital letters in the password.

We end up with a new list containing about 104mio words (2.2Go file) that we can now use with Hashcat to crack the keystore. The hash mode to use for a Java keystore is 15500. We add a rule to toggle the case of the characters. We can start with toggles1.rule that changes the case of 1 letter only, we can follow with toggles2.rule for 2 letters and up to toggles5.rule, if necessary.

$ hashcat --help | grep -i java
15500 | JKS Java Key Store Private Keys (SHA1)
$ hashcat -O -a 0 -m 15500 -r rules/toggles1.rule -o cracked.txt hash.txt wordlist_expanded.txt

We were lucky as there is only 1 capital letter and we got it cracked in less than 2 seconds (on a 4 years old MacBook Pro). The password is Howdygigem1.

Flag: gigem{Howdygigem1}


This post is for educational and awareness purpose only. You are solely responsible for any actions and/or activities related to the material contained within this post. I will not be held responsible in the event any criminal charges be brought against any individuals misusing the information in this blog to break the law.


Written by

Cyber Security Professional and CTFer from Switzerland.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade