Defenit CTF 2020 OSINT Challenge Author Writeup
Author's writeup for OSINT challenges themed around cryptocurrency and malware C2 servers.
This post was machine-translated from Korean by AI. Some expressions may differ from the original.
Read the original (Korean)Defenit CTF 2020 OSINT Challenge Write-Up (KOR,ENG)

I was in charge of the OSINT category for Defenit CTF 2020. Since my primary expertise is Web, I aimed to design challenges that combine Web and OSINT concepts. Before designing the challenges, I reflected on the field of OSINT. I concluded that the main purpose of OSINT is 'to acquire a hacker's information or identify them based on information published on the internet.' By linking this with recent trends, I established the main concept of preventing a hacker's malicious activities involving 'cryptocurrency' and 'malware.' The main concept for Bad Tumbler is 'cryptocurrency,' and for Hack the C2, it is 'ransomware C2 servers.' I will now describe the write-up for each problem.
1. Bad Tumblers
[TL;DR]

-
Parse given address recursively
-
Get characteristic of hacker address
-
Get connection of all parsed address (tumbler networks + hacker wallet + victim wallet + a)
-
Specify the wallet which has characteristic 'from some wallet but to less wallets (less than 5)'
-
Specify the wallet which had been having more than 400+ ether
This challenge addresses the recently trending issue of cryptocurrency money laundering. The key to solving this problem is tracking a hacker who stole cryptocurrency, commissioned laundering, and withdrew the funds.
First, the preconditions and explanation are as follows:
[Precondition] 0. Hundreds of wallets contain about 5 ether (tumbler) 0. Hackers steal more than 400 ethers through hacking per exchange 0. Hacker commissions ethereum tumbler to mix/tumble 400 ether from his wallet 0. After tracking the hacking accident that reported by exchange A, we confirmed that it was withdrawn to the hacker wallet of exchange C. 0. After checking the amount withdrawn from the wallet of exchange C, it seems that it was not the only exchange (exchange A) that was robbed. 0. Therefore, it is a problem to find a hacker wallet of some exchange (in this chall, it will be exchange B). So, we should find clues to track the hacker.
[Description] Hacker succeeded in hacking and seizing cryptocurrency, Ethereum! Hacker succeeded in withdraw Ethereum by mixing & tumbling. What we know is the account of the hacker of exchange A which reported the hacking and exchange C which hacker withdrew money. In addition to Exchange A, there is another exchange that has been hacked. Track hackers to find out the bad hacker's wallet address on another exchange!

It's quite long, isn't it? :( Sorry about that. I rambled a bit to construct a better and more plausible scenario...
The main contents of the preconditions and description can be understood more intuitively through the attached concept map. There are victims at Exchanges A and B who first transfer Ethereum to the hacker's wallet. The hacker transmits the collected Ethereum in small portions to the tumbler network. The transferred money circulates within the tumbler network and is eventually deposited gradually into the hacker's account at Exchange C. In short, you can understand it in a context similar to 'pouring water from a cup into a lake, and then scooping the water from the lake back into another cup.'
In this challenge, the hacker wallets for Exchange A and Exchange C are provided. Due to the nature of cryptocurrency, all transactions in each wallet are recorded and shared with everyone. The first step in solving the problem is to analyze the hacker wallet of the hacked Exchange A to investigate its characteristics, find all wallets connected to these wallets, and apply the characteristics found earlier to examine those wallets.

These are the initial transactions of the hacker's wallet from Exchange A, given as a condition of the problem.

In the Concept Map, this corresponds to the transaction shown above. It depicts the situation where victims' funds are deposited into the hacker's wallet. Looking at the transactions, you can see that the victims' Ethereum is first collected in the hacker's wallet, and then these Ethereums are withdrawn to the tumbler network.
"Hacker Wallet's Characteristic: many victims deposit to hacker wallet"
Now, we proceed with a recursive parse to analyze all wallets and investigate their characteristics. We collect connection information between wallets (nodes) by examining the from/to transactions among the collected wallets. Among this information, we filter by applying the characteristic we discovered: 'a wallet with few to transactions is a victim wallet, and the wallet corresponding to that to is a hacker wallet.' Subsequently, among the filtered wallets, we filter for a wallet that once held more than 400 Ethereum (max current value).

solve.py
https://github.com/JaewookYou/2020-defenit-ctf-osint-writeup/blob/master/bad-tumblers/solve.py
2. hack-the-c2
[TL;DR]
-
Find nickname at twitter
-
See deleted tweet from web.archive.org
-
Find ransomware name at github
-
See private repository (which is code of c2 server) from web.archive.org
-
Get url of c2 server and its code
-
Bypass curl argument's filter regex by idna normalization
( file:/‥/‥/, fi(‥, 1byte) will change to fi(\x46\x49, 2byte) after idna decoding at python )
-
Get internal server (which use 7777 port)'s code
-
Know it has connection with internal network's mysql server (172.22.0.4:3306)
-
When connect to mysql server, there aren't login password
-
So we can ssrf it by gopher scheme
-
Double url encoding because our packet via external server (which use 9090 port)
-
Get killcode from mysql, input killcode => FLAG XD
Description Some hacker make ransomware, and he is going to spread it. We should stop him, but the only we have is that the hacker uses nickname 'b4d_ar4n9'. Find hacker's info and stop him!
Hints
- my own github is not related to solve this challenge
This challenge was inspired by the British security researcher who prevented massive damage by activating a ransomware kill switch. The concept places us as security researchers who collect hacker information from the internet to leak the hacker's C2 server code. We then hack the C2 server using 'admin functions' created for the hacker's personal use and vulnerabilities arising from them, activating the kill switch to prevent the ransomware operation and save the world(?).

First, information must be collected to solve the problem. Searching for 'b4d_ar4n9' on Twitter reveals accounts and tweets using that nickname. Among these, there is a trace of the ransomware name being written and then deleted, as shown in the image above.
http://web.archive.org/web/20200520115408/https:/twitter.com/b4d_ar4n9

The ransomware name appears! Additionally, a recent tweet states, 'I committed all the C2 server code.' Therefore, let's search GitHub based on the ransomware name.

A repository belonging to an account identical to the hacker's nickname (in Leet speak) appears.
https://github.com/Ba6-4raNg/myfirstapp/commit/1c2c1140e17960aa5d81762d3176c10e2f13009c

Looking at the commits of that repository, there is a record stating, 'Made everything private, and archived all important info.'
http://web.archive.org/web/20200604115819/https://github.com/Ba6-4raNg

Ta-da! Here exists a repository with the ransomware name we saw earlier.

The repository contains the address of the C2 server (http://hack-the-c2.ctf.defenit.kr:9090/) http://hack-the-c2.ctf.defenit.kr:9090/) and its code.
# health check! - ps
@app.route('/he41th_ch3ck_C2_ps')
def health_ps():
r = subprocess.Popen("ps -ef".split(' '),stdout=subprocess.PIPE).stdout.read().decode().split('\n')
result = []
for i in r:
if 'python' in i:
result.append(i)
return render_template('he41th_ch3ck_C2_ps.html', results=result)
# health check! - netstat
@app.route('/h3alTh_CHeCK_c2_nEtsTaT')
def health_netstat():
r = subprocess.Popen("netstat -lntp".split(' '),stdout=subprocess.PIPE).stdout.read().decode().split('\n')
return render_template('h3alTh_CHeCK_c2_nEtsTaT.html', results=r)http://hack-the-c2.ctf.defenit.kr:9090/he41th_ch3ck_C2_ps
http://hack-the-c2.ctf.defenit.kr:9090/h3alTh_CHeCK_c2_nEtsTaT

By accessing a route that is generally difficult to discover using the leaked code, we can obtain the process info and netstat info of the current C2 server. Through this, we can confirm there is one more flask server running on port 7777 in addition to port 9090. (Some people put a lot of effort into the 127.0.0.11 host using port 37159, but that service is the host managing DNS within the docker network. It is unrelated to the challenge and is information that can be found via Googling.)
# health check! - curl
@app.route('/He4ltH_chEck_c2_cur1')
def health_curl():
url = request.args.get('url')
try:
if url:
turl = filterUrl(url)
if turl:
url = turl
try:
buffer = BytesIO()
c = pycurl.Curl()
c.setopt(c.URL,url)
c.setopt(c.SSL_VERIFYPEER, False)
c.setopt(c.WRITEDATA,buffer)
c.perform()
c.close()
try:
result = buffer.getvalue().decode().split('\n')
except:
result = buffer.getvalue()
except Exception as e:
print('[x] curl err - {}'.format(str(e)))
result = ['err.....']
return render_template('He4ltH_chEck_c2_cur1.html', results=result)
else:
return render_template('He4ltH_chEck_c2_cur1.html', results=['nah.. url is error or unsafe!'])
except Exception as e:
print('[x] curl err2... - {}'.format(str(e)))
return render_template('He4ltH_chEck_c2_cur1.html', results=['nah.. you didn\'t give url'])
def filterUrl(url):
try:
# you may not read any file
if re.compile(r"(^[^:]{3}:)").search(url):
if re.compile(r"(^[^:]{3}:/[^(.|/)]/[^(.|/)]/)").search(url):
print('[+] curl url - {}'.format(url.replace("..","").encode('idna').decode().replace("..","")))
return url.replace("..","").encode('idna').decode().replace("..","")
elif re.compile(r"(^[^:]{4}://(localhost|172\.22\.0\.\d{1,3})((:\d{1,5})/|/))").search(url):
p = parse.urlparse(url)
if (p.scheme == 'http') and p.netloc.split(':')[0] == 'localhost':
print('[+] curl url - {}'.format(url))
return url
elif re.compile(r"(^[^:]{6}://(localhost|172\.22\.0\.\d{1,3})((:\d{1,5})/|/))").search(url):
print('[+] curl url - {}'.format(url))
return url
except Exception as e:
print('[x] regex err - {}'.format(str(e)))
return False
return FalseThis is the most important part: there is a service that takes a user's input URL and passes it as an argument to curl. However, since the user input is filtered via regex, the author's intent is to bypass this and perform SSRF (Server Side Request Forgery). (Fortunately, no one solved the challenge with an unintended method.)
if re.compile(r"(^[^:]{3}:)").search(url):
if re.compile(r"(^[^:]{3}:/[^(.|/)]/[^(.|/)]/)").search(url):
print('[+] curl url - {}'.format(url.replace("..","").encode('idna').decode().replace("..","")))
return url.replace("..","").encode('idna').decode().replace("..","")Looking at the filtering code when the scheme is 3 characters long, you can verify that it performs idna normalization after removing '..' from the input URL. It is common for special unicode characters to be converted into printable ascii characters during idna normalization. The intention of this problem was to fuzz this idna normalization across the Unicode range to to acquire the Gadget needed to solve the challenge.

I also possess data obtained by fuzzing to find special cases in idna normalization; among them, fi (fi) can be used to solve this problem. This character is 1 byte before idna normalization, but changes to a 2-byte ascii character after normalization. Because of this, it passes the initial 3-character scheme filtering regex, and after idna normalization, the file:/// scheme is used, enabling a local file leak.
http://hack-the-c2.ctf.defenit.kr:9090/He4ltH_chEck_c2_cur1?url=file:/‥/‥/app2/app/main.py

With this, we have obtained the code for the internal flask server running on port 7777.
def connect_db():
db = pymysql.connect(
user='b4d_aR4n9',
#passwd=os.environ['DBPW'],
host='172.22.0.4',
port=3306,
db='defenit_ctf_2020',
charset='utf8'
)
return db
db = connect_db()
# if input killcode, kill all ransomware
@app.route('/k1ll_r4ns0mw4r3')
def kill_ransom():
try:
if request.remote_addr != '172.22.0.3' and request.remote_addr != '127.0.0.1':
return '[INTERNAL] localhost only..'
cursor = db.cursor(pymysql.cursors.DictCursor)
cursor.execute("SELECT ki11c0d3 from secret;")
if cursor.fetchall()[0]['ki11c0d3'] == request.args.get('ki11c0d3'):
return subprocess.Popen("/app2/getFlag", stdout=subprocess.PIPE).stdout.read().strip()
else:
return '[x] you put wrong killcode!'
except:
return '[x] errr.....'Examining the code, we can see that it connects to MySQL at 172.22.0.4:3306, reads the killcode, and if the killcode matches, it executes /app2/getFlag to output the flag.
elif re.compile(r"(^[^:]{6}://(localhost|172\.22\.0\.\d{1,3})((:\d{1,5})/|/))").search(url):
print('[+] curl url - {}'.format(url))
return urlRegarding the 6-character scheme in the external flask code, the host is restricted only to localhost or the 172.22.0.0/24 range. Using this, if we send a raw MySQL packet to gopher://172.22.0.4:3306/, we can input an arbitrary SQL query and retrieve the result. (Since MySQL is configured to authenticate without a password upon connection, authentication is possible even without the 20-byte hash seed value received during the handshake.)

The remaining process can be easily solved using Gopherous. (I was researching ways to prevent this, but ran out of time ㅠㅠ)

When requesting the MySQL server using the gopher scheme with that payload, you can confirm that the result comes back in bytes.

If you print this out, you can verify the killcode.

GET FLAG!!!
Epilogue.
It feels like this is the first time I've properly set my mind to creating challenges for a CTF. I was worried and anxious at first because I was creating questions for a field other than my main expertise, but after finishing it, I felt a small sense of satisfaction (?) that I seemed to have created problems suitable for the OSINT category. I'm not sure how it was for those who solved it. Was it okay? It's a pity that I couldn't create a more fun challenge than this due to it being more difficult to make than I initially thought, but if I get the chance, I would like to realize those ideas later and share them as problems. Thank you so much to everyone who solved them, and I apologize if there were any shortcomings. Thank you. Bye!
I have uploaded the script/solver files used to create the 'bad-tumblers' challenge, as well as the Docker and source code for the 'hack-the-c2' challenge, so if you want to configure it yourself, please take a look!
https://github.com/JaewookYou/2020-defenit-ctf-osint-writeup
made by @arang
Comments