<OOO>

LINEctf 2023 web 풀이 본문

CTF write up

LINEctf 2023 web 풀이

<OOO> 2023. 3. 28. 02:37

이번 2023년 linectf에서 문제를 복습하는 차원으로 풀어보고자 한다.

 

[Baby Simple GoCurl]

뭔가 SSRF 같이 생긴 문제다.

 

소스코드 - main.go

package main

import (
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/gin-gonic/gin"
)

func redirectChecker(req *http.Request, via []*http.Request) error {
	reqIp := strings.Split(via[len(via)-1].Host, ":")[0]

	if len(via) >= 2 || reqIp != "127.0.0.1" {
		return errors.New("Something wrong")
	}

	return nil
}

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, "%")) {
			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("[+] 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()
}

 

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)

main.go에 있는 소스코드중 /curl 페이지에 요청하는 코드이다.

내가 url과 header_key, header_value를 입력하면  curl 페이지로 넘어가서 GET 방식으로 요청을 한다.

 

필터링은 다음과 같다.

if c.ClientIP() != "127.0.0.1" && (strings.Contains(reqUrl, "flag") || strings.Contains(reqUrl, "curl") || strings.Contains(reqUrl, "%")) {
			c.JSON(http.StatusBadRequest, gin.H{"message": "Something wrong"})
			return
		}

ClientIP가 127.0.0.1 이여야 하고, url에 flag, curl, %를 사용하지 않으면 /flag/에 요청할 수 있다.

 

내가 풀지 못했던 이유를 생각해보면 flag 단어 필터링을 우회 할 수 있는 방법이 있을거다! 라는 이상한 확신을 가졌고,

우리가 다 아는 우회 방법이지만, 너무 어렵게 생각을 했던것 같기도 하다.

 

여기서 X-Forwarded-For 헤더를 사용하면 되는 문제였다.

 

아아....

이게 되는 이유가 if문에서 c.ClientIP() != '127.0.0.1' && ~~~~~ 구문이 있는데 c.ClientIP()가 127.0.0.1 이면 뒤에 오는 부분을 검증 하지 않는 것 같다.

 

만약 if문의 필터링에서 %를 필터링 하지 않았다면, percent encoding을 써서 /flag/를 만들어도 풀렸지 않았을까 싶다.

 

 

[Old Pal]

소스코드

#!/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) {
    print "Too long :(";
    die();
}
if ($pw =~ /[^0-9a-zA-Z_-]/) {
    print "Illegal character :(";
    die();
}
if ($pw !~ /[0-9]/ || $pw !~ /[a-zA-Z]/ || $pw !~ /[_-]/) {
    print "Weak password :(";
    die();
}
if ($pw =~ /[0-9_-][boxe]/i) {
    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) {
    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();
};

perl 로 만들어졌다.

if 문으로 필터링을 여러번 하는데, 20글자를 넘으면 안되고, 숫자+영소문자+영대문자+_ 조합이면 안되고, 패스워드가 숫자로 이루어져 있거나 영어만 이루어져 있거나 _로 이루어져 있으면 안되고, 숫자_boxe(?) 로 이루어져 있으면 안되고 ~~~~ 등 필터링이 많다.

그리고 eval 함수로 내가 입력한 password 값이 20230325가 맞으면 flag를 출력해준다.

 

perl에서 특수 리터럴 이라는게 존재하는데 __FILE__이나 __LINE__ 같은 것이 존재한다고 한다.

https://perldoc.perl.org/functions/__LINE__

 

__LINE__ - Perldoc Browser

 

perldoc.perl.org

 

__LINE__은 현재 줄 번호의 값을 가져와서 리턴을 하게 된다고 한다.

근데 테스트 했을 땐 1이 아닌 다른 값들이 나와버려서 뭐지 싶었다.

 

이렇게 간단한 코드를 짠 뒤 실행을 시켜보면

 

이렇게 내가 원하는 값인 20230325가 아닌 20230321이 나오게 된다.

__LINE__의 값이 5로 나왔다는 소리다.

 

 

아무리 봐도 이해가 잘 안되어서 테스트 하는 중에 내가 넣은 값이 더블쿼터(") 안에 들어가는 값일 경우? 라는 생각이 들어 테스트를 했다.

 

아....................

 

지금까지 드는 생각은 더블쿼터나 싱글쿼터 같은 문자열 처리 방식이 아닌 __LINE__을 쓰게 된다면 현재 줄 번호를 리턴 해주지만, 만약 "" 같은 더블쿼터, 싱글쿼터 등의 문자열 처리를 해준다면 eval 함수로 실행을 했을 때 무조건 1이 나오는 것으로 이해가 된 것 같다.(아닐수도 있지만....)

 

그래서 password 파라미터에 들어가는 값이 20230326-__LINE__ 이다.

 

신기하다....

 

 

 

[Adult Simple GoCurl]

Baby Simple GoCurl의 심화단계 버전이다.

baby와 다른 점은 

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 strings.Contains(reqUrl, "flag") || strings.Contains(reqUrl, "curl") || strings.Contains(reqUrl, "%") {
			c.JSON(http.StatusBadRequest, gin.H{"message": "Something wrong"})
			return
		}

c.ClientIP() 가 사라졌다.

이번에도 ip가 127.0.0.1 이면 /flag/ 페이지에 요청을 할 수 있다.

 

풀이 방법은 다음과 같다.

https://github.com/gin-gonic/gin/issues/2916

 

Respect the X-Forwarded-Prefix header (for redirects) · Issue #2916 · gin-gonic/gin

TL;DR Add support for the X-Forwarded-Prefix for improved support for reverse proxies. Redirects should always be relative to X-Forwarded-Prefix, not only in RedirectTrailingSlash. Description Redi...

github.com

먼저 여길 보면 gin 에서 X-Forwarded-Prefix 헤더를 통해 localhost로 인식 시킬 수 있다고 한다.

즉, 이 문제는 X-Forwarded-Prefix 헤더를 사용하여 풀으라는 것 같다.

 

 

 

 

 

 

+ 추가로 더 업로드 할 예정

 

[참조]

https://perldoc.perl.org/perldata#Version-Strings

 

https://github.com/gin-gonic/gin/issues/2916

 

https://project-euphoria.dev/blog/40-line-ctf-2023/

 

https://nanimokangaeteinai.hateblo.jp/entry/2023/03/30/173000

 

https://blog.maple3142.net/2023/03/26/line-ctf-2023-writeups/

 

'CTF write up' 카테고리의 다른 글

pdftex(latex) 에서 RCE 하기  (0) 2023.12.30
Urmia CTF 2023 web writeup  (0) 2023.09.04
2023 Incognito 4.0 CTF Writeup  (2) 2023.02.18
MCH2022 web writeup  (2) 2022.07.27
2021 Christmas CTF WEB writeup  (0) 2021.12.30
Comments