Defenit CTF 2020 OSINT 出题者 Writeup
以加密货币和恶意代码 C2 服务器为主题的 OSINT 题目出题者解析
本文由 AI 从韩语机器翻译,部分表达可能与原文有所不同。
阅读原文(韩语)Defenit CTF 2020 OSINT Challenge Write-Up (KOR,ENG)

在这次 Defenit CTF 2020 中,我负责了 OSINT 领域的出题。由于我本来的主攻领域是 Web,所以我以制作一道能够把 Web 与 OSINT 结合起来的题目为目标来构思题目。在构思题目之前,我思考了一下 OSINT 这个领域。我认为 OSINT 这个领域的主要目的是'基于互联网上公开的信息来获取黑客的信息或锁定其身份',于是我把它与近期的趋势结合起来,以阻止黑客利用'加密货币'和'恶意代码'等进行恶意行为作为主要概念来构思并制作了题目。Bad Tumbler 的主要概念是'加密货币',Hack the C2 的主要概念是'恶意代码的 C2 服务器'。那么,下面我将逐题进行 Write-up 讲解。
In this Defenit CTF 2020, I took charge of the OSINT category. Originally, my main field was the web, so I was going to make the challenge that could be done by combining the web and OSINT. Before I thought about the problem, I thought about the category called OSINT. It was thought that the main purpose of the field of OSINT was to'acquire hacker's information based on information published on the Internet or to specify him', and by linking it with recent trends, hackers using 'cryptocurrency' and 'malware'. The main concept was to prevent malicious movements and devised and produced problems. 'Bad Tumbler's main concept is 'cryptocurrency', and 'Hack the C2's main concept are 'C2 server of ransomware'. Then let's describe Write-up one by one.
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 wallet (unless 5)'
-
specify the wallet which had been having more than 400+ ether
这个挑战是一道与近期成为热点的加密货币洗钱相关的题目。黑客通过黑客手段窃取加密货币并委托进行洗钱,在其提现之后对这样的黑客进行追踪,就是解决该题目的主要关键点。
首先,前提条件和说明如下
This is a problem related to the money laundering of cryptocurrency, which has recently become an issue. The key point of solving a major problem is that a hacker hacks a cryptocurrency, entrusts money laundering, and tracks the hacker in the withdrawal situation.
First of all, the prerequisites and explanations 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 tumbling 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 long, isn't it? In order to compose a better and more probable scenario, I've written weekly.
前提条件和描述的主要内容,可以通过所附的概念图更直观地理解。交易所 A 和 B 各自都有受害者,这些受害者先把以太坊转到黑客的钱包里,黑客再把汇集起来的以太坊一点一点地分批转入 tumbler network,这样转出去的钱在 tumbler network 中不断地转来转去,最后一点一点地汇入交易所 C 的黑客账户。一句话概括,可以用'把水装进杯子倒入湖中,再从湖里用另一个杯子把水舀出来'这样类似的脉络来理解。
The main contents of the precondition and description can be understood more intuitively through the attached conceptual image. There are victims on each of exchanges A and B, and these victims first transfer Ethereum to the hacker's wallet, the hacker transmits the collected Ethereum in small portions to the tumbler network, and the transferred money is circulated on the tumbler network and then exchanges C Will be gradually deposited into your hacker account. In a nutshell, you can understand it in a similar context to'after pouring water into a cup and pouring it into the lake, then floating the water from the lake back into another cup'.
题目中给出了交易所 A 和 C 的黑客钱包。由于加密货币的特性,每个钱包中的交易(Transaction) 都会被记录并共享给所有人。解题的第一步,是分析遭到入侵的交易所 A 的黑客钱包、调查其特征,然后找出所有与这些钱包相关联的钱包,并把刚才发现的特征套用到那些钱包上进行考察。
The challenge is given to the exchange A and C hacker wallets. Due to the principle of cryptocurrency, all transactions in each wallet are recorded and shared with all. The first step in solving the problem is to analyze the hacker wallet of the hacked exchange A to investigate its characteristics, find all the wallets associated with these wallets, and apply the features found earlier in those wallets.

这是作为题目条件给出的交易所 A 的黑客钱包的初始 transaction。
These are the initial transactions in the hacker wallet of exchange A given as a condition of the problem.

在 Concept Map 中,这对应于上图所示的交易。这是受害者的钱被存入黑客钱包的情形。观察这些 transaction 可以发现,受害者的以太坊首先汇集到黑客的钱包中,然后这些以太坊被提现到 tumbler network。
In the Concept Map, it is a transaction corresponding to the picture above. The victim's money is 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"
现在,为了解析所有钱包并调查其特征,我们进行 recursive parse。在收集到的钱包中,通过调查 from/to transaction 来收集钱包(node) 之间的连接信息。在这些信息中,套用我们已发现的特征——'to transaction 较少的钱包是 victim 钱包,而其 to 所对应的钱包就是黑客钱包'——来进行过滤。随后,在被过滤出来的钱包中,再过滤出曾经一度持有 400 以太坊以上的钱包(max current value)。
Now parse all wallets and proceed with recursive parse to investigate the features. Among the collected wallets, connection information between wallets is collected by examining from/to transactions. Among the information, we filter by applying the feature that we found out, 'a wallet with a small amount of to transaction is a victim wallet, and a wallet corresponding to that to be a hacker wallet'. Subsequently, the filtered wallet (max current value) that once had more than 400 Ethereum was filtered.

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(\u2025, 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
DescriptionSome 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
这个挑战的灵感来自此前那位通过触发勒索软件的 kill switch 而阻止了巨大损失的英国安全研究员,我以与之类似的概念制作了这道题。作为安全研究员的我们,从互联网上收集黑客的信息,泄露出该黑客的 C2 服务器代码,再利用其为自己专用而制作的一类'admin function'以及由此产生的漏洞来入侵 C2 服务器,触发 kill switch,阻止勒索软件运行,从而拯救世界(?)——这就是本题的概念。
This challenge was created by a British security researcher who ran a previous ransomware kill switch to prevent massive damage and I made this challenge as a similar concept. As a security researcher, we collect hacker's information from the Internet and get the hacker's C2 server code to hack the C2 server by hacking the C2 server with some kind of 'admin functions' and vulnerabilities. The concept is to save the world by activating it to prevent ransomware from working (?).

首先,为了解决这道题,必须收集信息。在 Twitter 上搜索 'b4d_ar4n9',就会出现正在使用该昵称的账户和推文。其中,可以看到如上图所示的、曾经写下勒索软件名称后又删除的痕迹。
First, we need to collect information to solve the problem. If you search for 'b4d_ar4n9' on Twitter, the accounts and tweets using the nickname will appear. Of these, the name of the ransomware that was written and erased looks like the image above.
http://web.archive.org/web/20200520115408/https:/twitter.com/b4d_ar4n9

勒索软件的名称出现了!此外,在最近的推文中还有'已经把 c2 服务器的代码全部提交了'这样的内容。因此,我们就以勒索软件的名称为线索在 github 上搜索一下吧。
The ransomware name comes up! In addition, a recent tweet says 'committed all code from c2 server'. So let's search based on the name of ransomware on github.

会出现一个账户与黑客昵称(以 leet lang 书写)相同的 repository。
You'll get a repository with the same account (with leet lang) as the hacker's nickname.
https://github.com/Ba6-4raNg/myfirstapp/commit/1c2c1140e17960aa5d81762d3176c10e2f13009c

查看该 repository 的提交记录可以发现,存在'已经把所有东西都设为了 private,并且所有重要信息都进行归档'这样的记录。
When looking at the commit of the repository, there is a record that 'everything is made private and all important information is archived'.
http://web.archive.org/web/20200604115819/https://github.com/Ba6-4raNg

铛铛——这里存在一个以刚才看到的勒索软件名称命名的 repository。
There is a repository in the name of ransomware you saw earlier.

该 repository 中有 c2 服务器的地址(http://hack-the-c2.ctf.defenit.kr:9090/) 及其代码。
The repository contains the address of the c2 server(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

通过泄露出来的代码,访问那些通常不易得知的 route,就可以得知当前 c2 服务器的 process info 和 netstat info。由此可以确认,内部除了 9090 端口之外,还有一个运行在 7777 端口上的 flask server。(这里有些人在使用 37159 端口的 127.0.0.11 主机上花了很多功夫,但该服务只是 docker 内部网络中管理 dns 的主机。它与题目无关,是 Google 一下就能查到的内容)
If you access a route that is not normally known through the leaked code, you can get the process info and netstat info of the current c2 server. Through this, you can see that there is one more flask server that goes back to port 7777 in addition to port 9090. (Here is a lot of effort on the 127.0.0.11 host that uses port 37159, but the service is a host that manages dns on the network inside the docker. It has nothing to do with the challenge and can be found when you google Content)
# 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 False这是最重要的部分:存在一个接收用户 input url 并将其作为 curl 参数传递的服务。不过,由于它用 regex 对用户的 input 进行 filtering,所以绕过它来实现 SSRF(Server Side Request Forgery) 就是出题者的 intent。(幸运的是,没有人以 unintent(非预期解)解出这道题)
The most important part, there is a service that takes the user's input url and passes it as an argument of curl. However, since the user's input is filtered with regex, it is the intent of the submitter to bypass this and SSRF (Server Side Request Forgery). (Fortunately, no one has solved the challenge with unintent)
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("..","")查看上述代码中 scheme 为 3 个字符时的 filtering code,可以发现一个奇特之处:它会先从 input url 中移除 '..',然后执行 'idna normalization'。进行 idna normalization 时,特殊 unicode 被转换为 printable ascii character 的情况屡见不鲜。本题的意图就是:对这种 idna normalization 在 \u0000 到 \uffff 的 Unicode 范围内进行 fuzzing,从而获得解决该题所需的 Gadget。
Looking at the filtering code when the scheme is 3 letters among the above codes, it can be confirmed that 'idna normalization' is performed after removing '..' from the input url. When performing idna normalization, special unicode is converted into printable ascii character. The intention of this issue was to fuzzing this idna normalization from the Unicode range \u0000 to \uffff to get the Gadget needed to solve that challenge.

我自己也为了获得 idna normalization 中的特殊 case 而进行了 fuzzing,并保有那些数据,其中 fi(\ufb01) 可以用于解决这道题。该字符在 idna normalization 之前是 1 字节,但在 normalization 之后会变成 2 字节的 ascii character。正因如此,它能够通过最初的 3 字符 scheme 过滤 regex,并在 idna normalization 之后使用 file:/// scheme,从而使 local file leak 成为可能。
I also fuzz to get a special case in idna normalization and have that data, of which fi(\ufb01) can be used to solve the problem. The character is 1 byte before idna normalization, but after normalization it is changed to a 2-byte ascii character. Because of this, the first 3-letter scheme is passed through the filtering regex, and after idna
normalization, a file:/// scheme is used, and a local file leak is possible.
http://hack-the-c2.ctf.defenit.kr:9090/He4ltH_chEck_c2_cur1?url=file:/‥/‥/app2/app/main.py

至此,我们获得了运行在 7777 端口上的 internal flask server 的代码。
So, we can obtain 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.....'查看该代码可以确认:它会连接到 172.22.0.4:3306 的 mysql,从那里读取 killcode,当 killcode 正确时,就执行 /app2/getFlag 并输出 flag。
elif re.compile(r"(^[^:]{6}://(localhost|172\.22\.0\.\d{1,3})((:\d{1,5})/|/))").search(url):
print('[+] curl url - {}'.format(url))
return url在 external flask 的代码中,对于 6 个字符的 scheme,host 被限制为只能是 localhost 或 172.22.0.0/24 网段。利用这一点,如果向 gopher://172.22.0.4:3306/ 发送 raw mysql packet 来发起请求,就可以输入我们想要的任意 sql query 并取回结果。(由于 mysql connection 被设置为可以无需密码进行认证,所以即使没有 handshake 时取回的 20byte 的 hash seed 值,也能够完成 authentication)
For the 6-letter scheme among the codes in the external flask, the host is limited to the localhost or 172.22.0.0/24 band. If you request by sending raw mysql packet to gopher://172.22.0.4:3306/ using this, you can enter any sql query we want and get the result. (Because it is set to authenticate without a password when connecting to mysql, authentication is possible without a hash seed value of 20 bytes received during handshake)

剩下的过程可以用 Gopherous 轻松解决。(为了防止这种做法我本来还在研究,但时间不够了 呜呜)
The rest of the process can be easily solved with Gopherous. (I was researching to prevent this, but I ran out of time)

用该 payload 通过 gopher scheme 向 mysql 服务器发起请求时,可以确认返回的结果值是以 byte 形式到来的。
When the request is requested to the mysql server using the gopher scheme as the corresponding payload, the result value can be checked as a byte.

将其输出出来,就可以确认 killcode。
If you print it out, you can see the killcode.

GET FLAG!!!
Epilogue.
像这样认真下定决心给 CTF 出题,这似乎还是第一次。接下了并非自己主攻领域的出题任务,起初也有很多担忧和纠结,但全部做完之后,我获得了一点小小的满足感(?),觉得自己好像做出了一道契合 OSINT 这个类别的题目,只是不太清楚解题的各位感觉如何。还可以吗?由于制作过程比最初设想的要困难,没能做出比现在更有意思的挑战,这一点很遗憾,但如果有机会,我想以后再把那些想法实现成题目并分享出来。真的非常感谢解出题目的各位,如果有不足之处,深表歉意。谢谢大家。那就先到这里!
It seems to be the first time I have properly thought about this and presented a problem to the CTF. I had a lot of worries and worries at first because I took the questions in a field other than the main one, but after I finished making it, I got a slight satisfaction (?) that it seemed to have created a problem that fits into the category called OSINT. Was it okay? It's a pity that I couldn't create a more challenging challenge than it is now because I have a harder time to make it than I thought at first, but if I have the chance, I would like to share it later as a problem. Thank you so much for releasing and sorry if there was any shortage. Thank you. Sure!
'bad-tumblers' 题目制作时使用的脚本/solver 文件,以及 'hack-the-c2' 题目的 docker 和源码我都已经上传,想要自己搭建环境尝试的各位请参考!
I uploaded the script/solver file used to create the'bad-tumblers' challenge, and the docker & source code of 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
评论