We participated SCTF last week, it was hosted by Syclover, if interested can try it out on https://sctf2021.xctf.org.cn/
I only managed to solve 4 challenges, here are some of the writeups
Challenges
Loginme
Description
http://124.71.166.197:18001/
Try to loginme!
Challenge file
Goto the website http://124.71.166.197:18001/:
Click the link below, it say error 401 (means unauthorized)
After I look at the source code, found that middleware.go
is where it stop us to the link:
package middleware
import (
"github.com/gin-gonic/gin"
)
func LocalRequired() gin.HandlerFunc {
return func(c *gin.Context) {
if c.GetHeader("x-forwarded-for") != "" || c.GetHeader("x-client-ip") != "" {
c.AbortWithStatus(403)
return
}
ip := c.ClientIP()
if ip == "127.0.0.1" {
c.Next()
} else {
c.AbortWithStatus(401)
}
}
}
Can see it only check for x-forwarded-for
and x-client-ip
header
By searching Bypass client IP
, will lead to this StackOverflow link
It says we can use X-Forwarded-For, X-Real-IP, True-Client-IP
to bypass the check for localhost (127.0.0.1) IP address
By using Burp Suite to add the X-Real-IP
it works!
By changing the id
parameter, the server response changes
After I investigate and reading the source code, found that it was using template to generate the website page:
package route
import (
_ "embed"
"fmt"
"html/template"
"loginme/structs"
"loginme/templates"
"strconv"
"github.com/gin-gonic/gin"
)
func Index(c *gin.Context) {
c.HTML(200, "index.tmpl", gin.H{
"title": "Try Loginme",
})
}
func Login(c *gin.Context) {
idString, flag := c.GetQuery("id")
if !flag {
idString = "1"
}
id, err := strconv.Atoi(idString)
if err != nil {
id = 1
}
TargetUser := structs.Admin
for _, user := range structs.Users {
if user.Id == id {
TargetUser = user
}
}
age := TargetUser.Age
if age == "" {
age, flag = c.GetQuery("age")
if !flag {
age = "forever 18 (Tell me the age)"
}
}
if err != nil {
c.AbortWithError(500, err)
}
html := fmt.Sprintf(templates.AdminIndexTemplateHtml, age)
if err != nil {
c.AbortWithError(500, err)
}
tmpl, err := template.New("admin_index").Parse(html)
if err != nil {
c.AbortWithError(500, err)
}
tmpl.Execute(c.Writer, TargetUser)
}
And can see it follow the name
and age
in the structs.go file:
package structs
type UserInfo struct {
Id int
Username string
Age string
Password string
}
var Users = []UserInfo{
{
Id: 1,
Username: "Grandpa Lu",
Age: "22",
Password: "hack you!",
},
{
Id: 2,
Username: "Longlone",
Age: "??",
Password: "i don't know",
},
{
Id: 3,
Username: "Teacher Ma",
Age: "20",
Password: "guess",
},
}
var Admin = UserInfo{
Id: 0,
Username: "Admin",
Age: "",
Password: "flag{}",
}
As you can see, the Admin’s Password
is the flag!
But how do we get it? The website only prints Id
and Age
Solving
By searching Golang SSTI
will lead to this link: https://www.onsecurity.io/blog/go-ssti-method-research/
It actually vulnerable to SSTI attack (Server Side Template Injection)
The source code also shows that it is vulnerable:
html := fmt.Sprintf(templates.AdminIndexTemplateHtml, age)
if err != nil {
c.AbortWithError(500, err)
}
tmpl, err := template.New("admin_index").Parse(html)
if err != nil {
c.AbortWithError(500, err)
}
Follow the link above, putting GET parameter id=0
and age={{.Password}}
will show the flag
Easy flag!!
Flag
SCTF{E@zy_SIGn_Ch3eR!}
This_is_A_tree
Description
一颗圣诞树,还有好多礼物,flag需要SCTF{}噢 ,a beautiful tree,U need to know some Chinese traditional knowledge,flag need a “SCTF{your_flag}”
Hint
This_is_A_Tree New Hint :The text you see is very ancient Chinese knowledge. We call it "gua". The ancients combined it with destiny and prediction. And I combined it with base64, you can refer to https://zh.wikipedia.org/wiki/%E5%85%AD%E5%8D%81%E5%9B%9B%E5%8D%A6, Note that the "gua" is from bottom to top
Challenge file
Unzip it, saw many array
files nested inside left
and right
folders:
Archive: tree.zip
extracting: array
extracting: letf/array
extracting: letf/letf/array
extracting: letf/letf/letf/array
extracting: letf/letf/letf/letf/array
extracting: letf/letf/letf/letf/letf/array
extracting: letf/letf/letf/letf/Right/array
extracting: letf/letf/letf/Right/array
extracting: letf/letf/letf/Right/letf/array
extracting: letf/letf/letf/Right/Right/array
...
...
creating: Right/Right/Right/Right/Right/
extracting: Right/Right/Right/Right/Right/array
See one of the array
file looks like base64:
cat array
# Q2hp
Therefore, I tried to cat
all array files together by using the command:
find ./ -name array -exec cat {} \;
# Q2hpbmVzZSB0cmFkaXRpb25hbCBjdWx0dXJlIGlzIGJyb2FkIGFuZCBwcm9mb3VuZCEgU28gSSBXYW50IEdpdmUgWW91IE15IEZsYWcgQnV0IFlvdSBOZWVkIERlY29kZSBJdC5FbmpveSBUaGUgRmxhZyEhOuW4iCDlhZEg5aSNIOaNnyDlt70g6ZyHIOaZiyDlp6Qg5aSn6L+HIOiuvCDlmazll5Eg6ZyHIOaBkiDoioIg6LGrIA==
Yeah! Confirm is Base64 cause ==
at the end
Pipe it with base64 -d
getting:
find ./ -name array -exec cat {} \;| base64 -d
# Chinese traditional culture is broad and profound! So I Want Give You My Flag But You Need Decode It.Enjoy The Flag!!:师 兑 复 损 巽 震 晋 姤 大过 讼 噬嗑 震 恒 节 豫
As a chinese I don’t even know what is it LOL XD
After received the hint, then I know how to decode it
Basically, you have to find which character represent the coresponding base64 symbol
For example: 師
is Q
, 兌
is 2
etc.
Solving
Solving this is abit troublesome for non-chinese people, because the text is simplified chinese, but the wikipedia page is tradisional chinese
Anyway, you can change to simplified chinese by click here:
Then copy the whole row beside 六十四卦
:
Then follow the Base64 character index to decode the flag!!
I wrote the script in Python:
import string
gua = ['坤','剥','比','观','豫','晋','萃','否','谦','艮','蹇','渐','小过','旅','咸','遁','师','蒙','坎','涣','解','未济','困','讼','升','蛊','井','巽','恒','鼎','大过','姤','复','颐','屯','益','震','噬嗑','随','无妄','明夷','贲','既济','家人','丰','离','革','同人','临','损','节','中孚','归妹','睽','兑','履','泰','大畜','需','小畜','大壮','大有','夬','乾']
b64 = string.ascii_uppercase + string.ascii_lowercase + string.digits + "+/"
text = "师 兑 复 损 巽 震 晋 姤 大过 讼 噬嗑 震 恒 节 豫".split()
flag = ''
for t in text:
flag += b64[gua.index(t)]
print(flag)
# Q2gxbkFfeXlkcyE
Pipe it into base64 -d
will the flag!
python3 solve.py | base64 -d
# Ch1nA_yyds!base64: invalid input
Flag
SCTF{Ch1nA_yyds!}
Godness Dance
Challenge file
We are given an Linux executable file (ELF) and is stripped (no debug info):
dance.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1de3e5d06173644a6c8d0a65e085646c1ebc9a9f, for GNU/Linux 3.2.0, stripped
Try run it:
./dance.out
Input:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Count wrong!
Open it with Ghidra and decompile it:
undefined8 FUN_00101100(void)
{
char cVar1;
uint uVar2;
long i;
char *pointer;
char *pointer2;
char *buffer;
long in_FS_OFFSET;
undefined uStack2056;
char buffer [28];
char local_7eb [1979];
long local_30;
local_30 = *(long *)(in_FS_OFFSET + 0x28);
buffer = buffer;
__printf_chk(1,"Input:");
pointer = buffer;
do {
pointer2 = pointer + 1;
__isoc99_scanf(&DAT_0010201f,pointer);
pointer = pointer2;
} while (pointer2 != local_7eb);
if (0 < DAT_00107ed4) {
i= DAT_00107ed4 - 1;
pointer = buffer;
do {
cVar1 = *pointer;
pointer = pointer + 1;
(&array)[cVar1 + -0x61] = (&array)[cVar1 + -0x61] + 1;
} while (pointer != buffer + (ulong)i+ 1);
}
i = 0;
do {
if (*(int *)((long)&array + i) != *(int *)((long)&DATA + i)) {
FUN_00101360();
goto LAB_00101260;
}
i = i + 4;
} while (i != 0x68);
FUN_00101400(&uStack2056,0x1c,200);
i = 4;
do {
if (*(int *)(&DAT_0010dd20 + i) != *(int *)(&DAT_00104020 + i)) goto LAB_00101260;
i = i + 4;
} while (i != 0x74);
__printf_chk(1,"Good for you!\nflag:SCTF{");
do {
cVar1 = *buffer;
buffer = buffer + 1;
putchar((int)cVar1);
} while (buffer != local_7eb);
putchar(0x7d);
if (local_30 == *(long *)(in_FS_OFFSET + 0x28)) {
return 0;
}
LAB_00101267:
/* WARNING: Subroutine does not return */
__stack_chk_fail();
LAB_00101260:
FUN_00101390();
goto LAB_00101267;
}
The code looks quite messy, let me make it more readble:
undefined8 FUN_00101100(void)
{
char cVar1;
uint i;
long i;
char *pointer;
char *pointer2;
char *buffer;
long in_FS_OFFSET;
undefined uStack2056;
char buffer [28];
char local_7eb [1979];
long local_30;
__printf_chk(1,"Input:");
pointer = buffer;
do {
pointer2 = pointer + 1;
__isoc99_scanf("%c",pointer);
pointer = pointer2;
} while (pointer2 != local_7eb);
pointer = buffer;
do {
cVar1 = *pointer;
pointer++;
array[cVar1 -0x61] = array[cVar1 -0x61] + 1;
} while (pointer != buffer + 29);
i = 0;
do {
if (array[i] != DATA[i]) {
__printf_chk(1,"Count wrong!");
exit(0);
}
i = i + 4;
} while (i != 0x68);
FUN_00101400(&uStack2056,0x1c,200);
i = 4;
do {
if (DAT_0010dd20[i] != DATA2[i]){
__printf_chk(1,"Wrong!");
exit(0);
}
i = i + 4;
} while (i != 0x74);
__printf_chk(1,"Good for you!\nflag:SCTF{");
do {
cVar1 = *buffer;
buffer = buffer + 1;
putchar((int)cVar1);
} while (buffer != local_7eb);
putchar(0x7d);
}
First Check
First it take our input and count the number of character we input, and store it in array
And we know 0x61(97) is a
in ASCII, means first index of array is a
2nd b
third c
and so on
Then it compare to DATA
if not equal will print wrong
Goto Ghidra, double click DAT_00105f80
hightlight it and right click > Copy as byte string:
01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01
Now we can base on this data and generate a valid input!!
Can easily guessed, it contains all alphabet letters and i
and u
occur twice
The valid input should be abcdefghiijklmnopqrstuuvwxyz
:
./dance.out
Input:abcdefghiijklmnopqrstuuvwxyz
Wrong!
Yeah! Looks like we pass the first check!
Second Check
After the first check, it pass the input to FUN_0010140
function (Quite complicated function)
I decided to analyse dynamicly, since the function is hard to reverse
I use GDB to help me
Set a breakpoint after the function (base address 0x555555555000):
b * 0x5555555551d8
Then the proceed input is at rip + 0xcb3c
Then type x/32w $rip + 0xcb3c
to view the address memory 32 words (32*4bytes)
pwndbg> x/32w $rip + 0xcb3c
0x555555561d14: 0x00000000 0x00000000 0x00000000 0x00000000
0x555555561d24: 0x00000001 0x00000002 0x00000003 0x00000004
0x555555561d34: 0x00000005 0x00000006 0x00000007 0x00000008
0x555555561d44: 0x00000009 0x0000000a 0x0000000b 0x0000000c
0x555555561d54: 0x0000000d 0x0000000e 0x0000000f 0x00000010
0x555555561d64: 0x00000011 0x00000012 0x00000013 0x00000014
0x555555561d74: 0x00000015 0x00000016 0x00000017 0x00000018
0x555555561d84: 0x00000019 0x0000001a 0x0000001b 0x0000001c
We can see it is a sequence number from 1 to 28
Lets try swap some characeters and observe how the data changes
I swap a
and b
:bacdefghiijklmnopqrstuuvwxyz
Run it and the data shows:
pwndbg> x/32w $rip + 0xcb3c
0x555555561d14: 0x00000000 0x00000000 0x00000000 0x00000000
0x555555561d24: 0x00000002 0x00000001 0x00000003 0x00000004
0x555555561d34: 0x00000005 0x00000006 0x00000007 0x00000008
0x555555561d44: 0x00000009 0x0000000a 0x0000000b 0x0000000c
0x555555561d54: 0x0000000d 0x0000000e 0x0000000f 0x00000010
0x555555561d64: 0x00000011 0x00000012 0x00000013 0x00000014
0x555555561d74: 0x00000015 0x00000016 0x00000017 0x00000018
0x555555561d84: 0x00000019 0x0000001a 0x0000001b 0x0000001c
As you can see, the 1st and 2nd element changes
At first I thought it is the ASCII value, just plus 97
But when I look carefully, it is actually the index of our original input abcdefghiijklmnopqrstuuvwxyz
Because it has 28 elements so should not be ASCII value, then it must be index
change input to zbcdefghiijklmnopqrstuuvwxya
, as you can see the 1st element is 28 (a
is 28th character in our input), and last element is 1 (z
is 1st character in our input)
x/32w $rip + 0xcb3c
0x555555561d14: 0x00000000 0x00000000 0x00000000 0x00000000
0x555555561d24: 0x0000001c 0x00000002 0x00000003 0x00000004
0x555555561d34: 0x00000005 0x00000006 0x00000007 0x00000008
0x555555561d44: 0x00000009 0x0000000a 0x0000000b 0x0000000c
0x555555561d54: 0x0000000d 0x0000000e 0x0000000f 0x00000010
0x555555561d64: 0x00000011 0x00000012 0x00000013 0x00000014
0x555555561d74: 0x00000015 0x00000016 0x00000017 0x00000018
0x555555561d84: 0x00000019 0x0000001a 0x0000001b 0x00000001
Therefore, we can take the DATA2
and generate our input!
Solving
Hightlight the DAT_00104024
in Ghirda and right click > Copy as Byte string:
020000001a000000110000001c000000180000000b000000150000000a000000100000001400000013000000120000000300000008000000060000000c000000090000000e0000000d00000016000000040000001b0000000f0000001700000001000000190000000700000005
Then delete all 00
, write a python script to generate:
text = bytes.fromhex("021a111c180b150a101413120308060c090e0d16041b0f1701190705")
c = "abcdefghiijklmnopqrstuuvwxyz"
flag = bytearray(28)
for i in range(28):
flag[text[i]-1] = ord(c[i])
print(flag)
# bytearray(b'waltznymphforquickjigsvexbud')
Yeah!! Try with the binary, and we got the flag!!:
./dance.out
Input:waltznymphforquickjigsvexbud
Good for you!
flag:SCTF{waltznymphforquickjigsvexbud}
Flag
SCTF{waltznymphforquickjigsvexbud}