Featured image

Adventures in Version Control Link to heading

Version control is awesome, it really is! Especially Git. But sometimes, site admins or devs make stupid mistakes, and one of these mistakes is failing to cleanup after one’s self when deploying.

In this simple room, the goal is to figure out which username and password combination will unlock the rest of the site. So let’s get cracking.

First Steps: nmap Link to heading

nmap is a universal first step to see what’s what is a room. It’ll help us get the lay of the land and may help find our first real foothold. So let’s see what we have:


nmap -sS -A $TARGET_IP
Starting Nmap 7.80 ( https://nmap.org ) at 2020-07-24 22:10 CEST
Nmap scan report for $TARGET_IP
Host is up (0.029s latency).
Not shown: 999 closed ports
PORT   STATE SERVICE VERSION
80/tcp open  http    nginx 1.14.0 (Ubuntu)
| http-git:
|   $TARGET_IP:80/.git/
|     Git repository found!
|_    Repository description: Unnamed repository; edit this file 'description' to name the...
|_http-server-header: nginx/1.14.0 (Ubuntu)
|_http-title: Super Awesome Site!
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.80%E=4%D=7/24%OT=80%CT=1%CU=42951%PV=Y%DS=2%DC=T%G=Y%TM=5F1B404
OS:B%P=x86_64-pc-linux-gnu)SEQ(SP=107%GCD=1%ISR=105%TI=Z%CI=Z%II=I%TS=A)OPS
OS:(O1=M508ST11NW7%O2=M508ST11NW7%O3=M508NNT11NW7%O4=M508ST11NW7%O5=M508ST1
OS:1NW7%O6=M508ST11)WIN(W1=F4B3%W2=F4B3%W3=F4B3%W4=F4B3%W5=F4B3%W6=F4B3)ECN
OS:(R=Y%DF=Y%T=40%W=F507%O=M508NNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=A
OS:S%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R
OS:=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F
OS:=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%
OS:T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD
OS:=S)

Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 587/tcp)
HOP RTT      ADDRESS
1   29.11 ms 10.11.0.1
2   29.17 ms $TARGET_IP

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 23.30 seconds

We can see that there’s only 1 server running on port 80, and an exposed git repository. This may come in handy later on.

Checking Out The Server Link to heading

So we can explore the server a bit with our good friend curl.


curl 10.10.124.73
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Super Awesome Site!</title>
    <link rel="stylesheet" href="/css/style.css">
  </head>
  <body>
    <h1>Login</h1>
    <form class="login-form" id="login-form">
     <div class="flex-row">
         <p class="error" id="error"></p>
     </div>
      <div class="flex-row">
        <label class="lf--label" for="username">
          <svg x="0px" y="0px" width="12px" height="13px">
            <path
              fill="#B1B7C4"
              d="M8.9,7.2C9,6.9,9,6.7,9,6.5v-4C9,1.1,7.9,0,6.5,0h-1C4.1,0,3,1.1,3,2.5v4c0,0.2,0,0.4,0.1,0.7 C1.3,7.8,0,9.5,0,11.5V13h12v-1.5C12,9.5,10.7,7.8,8.9,7.2z M4,2.5C4,1.7,4.7,1,5.5,1h1C7.3,1,8,1.7,8,2.5v4c0,0.2,0,0.4-0.1,0.6 l0.1,0L7.9,7.3C7.6,7.8,7.1,8.2,6.5,8.2h-1c-0.6,0-1.1-0.4-1.4-0.9L4.1,7.1l0.1,0C4,6.9,4,6.7,4,6.5V2.5z M11,12H1v-0.5 c0-1.6,1-2.9,2.4-3.4c0.5,0.7,1.2,1.1,2.1,1.1h1c0.8,0,1.6-0.4,2.1-1.1C10,8.5,11,9.9,11,11.5V12z"
            />
          </svg>
        </label>
        <input
          id="username"
          name="username"
          class="lf--input"
          placeholder="Username"
          type="text"
        />
      </div>
      <div class="flex-row">
        <label class="lf--label" for="password">
          <svg x="0px" y="0px" width="15px" height="5px">
            <g>
              <path
                fill="#B1B7C4"
                d="M6,2L6,2c0-1.1-1-2-2.1-2H2.1C1,0,0,0.9,0,2.1v0.8C0,4.1,1,5,2.1,5h1.7C5,5,6,4.1,6,2.9V3h5v1h1V3h1v2h1V3h1 V2H6z M5.1,2.9c0,0.7-0.6,1.2-1.3,1.2H2.1c-0.7,0-1.3-0.6-1.3-1.2V2.1c0-0.7,0.6-1.2,1.3-1.2h1.7c0.7,0,1.3,0.6,1.3,1.2V2.9z"
              />
            </g>
          </svg>
        </label>
        <input
          id="password"
          name="password"
          class="lf--input"
          placeholder="Password"
          type="password"
        />
      </div>
      <input class='lf--submit' type="button" value="LOGIN" onclick="login()" />
    </form>
   <script>
        const _0x4368=['+(\x20+[^','471197','value','RegExp','functi','test','CbRnH','passwo','userna','TML','tml','a865c5','+[^\x20]}','a5f298','cookie','admin','3a71fd','getEle','login-','^([^\x20]','TEhxP','href','f64cb3','51a151','d84319','D\x20USER','digest','R\x20PASS','oard.h','error','\x20]+)+)','19a3c0','f80f67','/dashb','bea070','3ec9cb','padSta','from','4004c2','WORD!','map','NAME\x20O','encode','INVALI','a5106e','baf89f','6a7c7c','elemen','9a88db','log','join','innerH','SaltyB','apply','ned','442a9d','mentBy'];(function(_0x1ef2d8,_0x436806){const _0x2c2818=function(_0x302738){while(--_0x302738){_0x1ef2d8['push'](_0x1ef2d8['shift']());}},_0x6f8b4a=function(){const _0x2e9681={'data':{'key':'cookie','value':'timeout'},'setCookie':function(_0x329b53,_0x28dc3d,_0x22f4a3,_0x6012c1){_0x6012c1=_0x6012c1||{};let _0x3d8f23=_0x28dc3d+'='+_0x22f4a3,_0x18026e=0x0;for(let _0x4175c9=0x0,_0x25d1be=_0x329b53['length'];_0x4175c9<_0x25d1be;_0x4175c9++){const _0x109e81=_0x329b53[_0x4175c9];_0x3d8f23+=';\x20'+_0x109e81;const _0x1e9a27=_0x329b53[_0x109e81];_0x329b53['push'](_0x1e9a27),_0x25d1be=_0x329b53['length'],_0x1e9a27!==!![]&&(_0x3d8f23+='='+_0x1e9a27);}_0x6012c1['cookie']=_0x3d8f23;},'removeCookie':function(){return'dev';},'getCookie':function(_0x3e797a,_0x2a5b7d){_0x3e797a=_0x3e797a||function(_0x242cdf){return _0x242cdf;};const _0x996bc1=_0x3e797a(new RegExp('(?:^|;\x20)'+_0x2a5b7d['replace'](/([.$?*|{}()[]\/+^])/g,'$1')+'=([^;]*)')),_0x51d0ee=function(_0x439650,_0x52fa41){_0x439650(++_0x52fa41);};return _0x51d0ee(_0x2c2818,_0x436806),_0x996bc1?decodeURIComponent(_0x996bc1[0x1]):undefined;}},_0x17997b=function(){const _0x383e88=new RegExp('\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*[\x27|\x22].+[\x27|\x22];?\x20*}');return _0x383e88['test'](_0x2e9681['removeCookie']['toString']());};_0x2e9681['updateCookie']=_0x17997b;let _0x39ee22='';const _0xad377=_0x2e9681['updateCookie']();if(!_0xad377)_0x2e9681['setCookie'](['*'],'counter',0x1);else _0xad377?_0x39ee22=_0x2e9681['getCookie'](null,'counter'):_0x2e9681['removeCookie']();};_0x6f8b4a();}(_0x4368,0xe6));const _0x2c28=function(_0x1ef2d8,_0x436806){_0x1ef2d8=_0x1ef2d8-0x0;let _0x2c2818=_0x4368[_0x1ef2d8];return _0x2c2818;};const _0x22f4a3=function(){let _0x36b504=!![];return function(_0x1087c7,_0x108f32){if(_0x2c28('0x4')===_0x2c28('0x4')){const _0x52d1da=_0x36b504?function(){if(_0x2c28('0x12')!==_0x2c28('0x12')){function _0x382a78(){document[_0x2c28('0xf')+_0x2c28('0x36')+'Id'](_0x2c28('0x1b'))['innerH'+_0x2c28('0x7')]=_0x2c28('0x29')+_0x2c28('0x17')+'NAME\x20O'+_0x2c28('0x19')+_0x2c28('0x25');}}else{if(_0x108f32){const _0x725292=_0x108f32[_0x2c28('0x33')](_0x1087c7,arguments);return _0x108f32=null,_0x725292;}}}:function(){};return _0x36b504=![],_0x52d1da;}else{function _0x323170(){const _0x2ed5f9=_0x36b504?function(){if(_0x108f32){const _0x407994=_0x108f32[_0x2c28('0x33')](_0x1087c7,arguments);return _0x108f32=null,_0x407994;}}:function(){};return _0x36b504=![],_0x2ed5f9;}}};}(),_0x28dc3d=_0x22f4a3(this,function(){const _0x5b8de6=typeof window!=='undefi'+_0x2c28('0x34')?window:typeof process==='object'&&typeof require===_0x2c28('0x2')+'on'&&typeof global==='object'?global:this,_0x4d9f75=function(){const _0x1eee2f=new _0x5b8de6[(_0x2c28('0x1'))](_0x2c28('0x11')+_0x2c28('0x37')+_0x2c28('0x1c')+_0x2c28('0xa'));return!_0x1eee2f[_0x2c28('0x3')](_0x28dc3d);};return _0x4d9f75();});_0x28dc3d();async function login(){let _0x110afb=document[_0x2c28('0xf')+_0x2c28('0x36')+'Id'](_0x2c28('0x10')+'form');console[_0x2c28('0x2f')](_0x110afb[_0x2c28('0x2d')+'ts']);let _0x383cb8=_0x110afb[_0x2c28('0x2d')+'ts'][_0x2c28('0x6')+'me'][_0x2c28('0x0')],_0x5b6063=await digest(_0x110afb[_0x2c28('0x2d')+'ts'][_0x2c28('0x5')+'rd'][_0x2c28('0x0')]);_0x383cb8===_0x2c28('0xd')&&_0x5b6063===_0x2c28('0x24')+_0x2c28('0xe')+'6ba9b0'+_0x2c28('0x21')+'7eed08'+_0x2c28('0x38')+_0x2c28('0x16')+_0x2c28('0x9')+_0x2c28('0x35')+_0x2c28('0x2c')+_0x2c28('0x20')+'f3cb6a'+_0x2c28('0x2a')+_0x2c28('0x1e')+_0x2c28('0x2e')+_0x2c28('0x2b')+_0x2c28('0x14')+_0x2c28('0x15')+_0x2c28('0xb')+_0x2c28('0x1d')+'94eceb'+'bb'?(document[_0x2c28('0xc')]='login='+'1',window['locati'+'on'][_0x2c28('0x13')]=_0x2c28('0x1f')+_0x2c28('0x1a')+_0x2c28('0x8')):document['getEle'+_0x2c28('0x36')+'Id'](_0x2c28('0x1b'))[_0x2c28('0x31')+_0x2c28('0x7')]=_0x2c28('0x29')+_0x2c28('0x17')+_0x2c28('0x27')+_0x2c28('0x19')+_0x2c28('0x25');}async function digest(_0x35521d){const _0x179c00=new TextEncoder(),_0x713734=_0x179c00[_0x2c28('0x28')](_0x35521d+(_0x2c28('0x32')+'ob')),_0x39b76f=await crypto['subtle'][_0x2c28('0x18')]('SHA-51'+'2',_0x713734),_0x558ac0=Array[_0x2c28('0x23')](new Uint8Array(_0x39b76f)),_0x34e00e=_0x558ac0[_0x2c28('0x26')](_0x468ec7=>_0x468ec7['toStri'+'ng'](0x10)[_0x2c28('0x22')+'rt'](0x2,'0'))[_0x2c28('0x30')]('');return _0x34e00e;}
  </script>
  </body>
</html>



So we have what looks like a login form and some heavily obfuscated javascript. This probably looks like a dead end. Or does it?

Silliness and Version Control Link to heading

With some luck (for us), the developpers left the entire history of the codebase open for all to see. We can abuse this and download the git repository using wget:


wget --mirror -I .git $TARGET_IP/.git/
Connecting to 10.10.124.73:80... connected.
HTTP request sent, awaiting response... 200 OK

...Some Time Later...

FINISHED --2020-07-24 22:33:48--
Total wall clock time: 3.1s
Downloaded: 48 files, 20K in 0s (55.0 MB/s)

I won’t put the whole command here because it’s kind of verbose and pointless. Basically because the directory listing was left on by inadvertence, we can easily mirror the site. Had this not been the case, we would have to rebuild the repository piece by bloody piece. And that’s just no fun at all.

If we enter the newly created directory, we can see a plain .git directory, and nothing else.


pwd
/home/hydra/.../git-happens/$TARGET_IP
ls -la
total 12
drwxr-xr-x 3 hydra hydra 4096 Jul 24 22:35 .
drwxr-xr-x 3 hydra hydra 4096 Jul 24 22:35 ..
drwxr-xr-x 8 hydra hydra 4096 Jul 24 22:35 .git

We can use the standard git commands to see what’s up:


git status
On branch master
Changes not staged for commit:
  (use "git add/rm ..." to update what will be committed)
  (use "git restore ..." to discard changes in working directory)
        deleted:    .gitlab-ci.yml
        deleted:    Dockerfile
        deleted:    README.md
        deleted:    css/style.css
        deleted:    dashboard.html
        deleted:    default.conf
        deleted:    index.html

no changes added to commit (use "git add" and/or "git commit -a")
git reset HEAD --hard
HEAD is now at d0b3578 Update .gitlab-ci.yml
ls -la
total 44
drwxr-xr-x 4 hydra hydra 4096 Jul 24 22:41 .
drwxr-xr-x 3 hydra hydra 4096 Jul 24 22:35 ..
drwxr-xr-x 2 hydra hydra 4096 Jul 24 22:41 css
-rw-r--r-- 1 hydra hydra 3775 Jul 24 22:41 dashboard.html
-rw-r--r-- 1 hydra hydra 1115 Jul 24 22:41 default.conf
-rw-r--r-- 1 hydra hydra  120 Jul 24 22:41 Dockerfile
drwxr-xr-x 8 hydra hydra 4096 Jul 24 22:41 .git
-rw-r--r-- 1 hydra hydra  792 Jul 24 22:41 .gitlab-ci.yml
-rw-r--r-- 1 hydra hydra 6890 Jul 24 22:41 index.html
-rw-r--r-- 1 hydra hydra   54 Jul 24 22:41 README.md

So here we used git status to get a feel for what’s going on, git reset to reset the repository state to what the master branch should be, and behold! files!

We see that the site has a dashboard.html file, let’s see if there’s a flag hiding there.


curl 10.10.124.73/dashboard.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Awesome!</title>
    <link rel="stylesheet" href="/css/style.css" />
  </head>
  <body onload="checkCookie()">
    <p class="rainbow-text">Awesome! Use the password you input as the flag!</p>
  <script>
      var _0x13f2=['The\x20co','test','href','okie\x20\x22','RegExp','locati','+[^\x20]}','\x20value','hvvqf','split','apply','\x20has\x20\x22','UmvzZ','+(\x20+[^','undefi','includ','functi','/index','object','login\x22','BzWoi','JSjhF','1\x22\x20for','.html','log'];(function(_0x25afd6,_0x13f2ae){var _0x293e02=function(_0x39bb51){while(--_0x39bb51){_0x25afd6['push'](_0x25afd6['shift']());}},_0x4e9f94=function(){var _0x664dc3={'data':{'key':'cookie','value':'timeout'},'setCookie':function(_0x5504a4,_0x228943,_0x1cdb91,_0x1e9670){_0x1e9670=_0x1e9670||{};var _0x39bcf8=_0x228943+'='+_0x1cdb91,_0x53f475=0x0;for(var _0x13cc7b=0x0,_0x264136=_0x5504a4['length'];_0x13cc7b<_0x264136;_0x13cc7b++){var _0x44c799=_0x5504a4[_0x13cc7b];_0x39bcf8+=';\x20'+_0x44c799;var _0x42ecce=_0x5504a4[_0x44c799];_0x5504a4['push'](_0x42ecce),_0x264136=_0x5504a4['length'],_0x42ecce!==!![]&&(_0x39bcf8+='='+_0x42ecce);}_0x1e9670['cookie']=_0x39bcf8;},'removeCookie':function(){return'dev';},'getCookie':function(_0x1baf49,_0x45f075){_0x1baf49=_0x1baf49||function(_0x2f4c45){return _0x2f4c45;};var _0x19df86=_0x1baf49(new RegExp('(?:^|;\x20)'+_0x45f075['replace'](/([.$?*|{}()[]\/+^])/g,'$1')+'=([^;]*)')),_0x264820=function(_0x344491,_0x554a27){_0x344491(++_0x554a27);};return _0x264820(_0x293e02,_0x13f2ae),_0x19df86?decodeURIComponent(_0x19df86[0x1]):undefined;}},_0x1feb05=function(){var _0x16aab2=new RegExp('\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*[\x27|\x22].+[\x27|\x22];?\x20*}');return _0x16aab2['test'](_0x664dc3['removeCookie']['toString']());};_0x664dc3['updateCookie']=_0x1feb05;var _0x4984a0='';var _0x29b714=_0x664dc3['updateCookie']();if(!_0x29b714)_0x664dc3['setCookie'](['*'],'counter',0x1);else _0x29b714?_0x4984a0=_0x664dc3['getCookie'](null,'counter'):_0x664dc3['removeCookie']();};_0x4e9f94();}(_0x13f2,0x102));var _0x293e=function(_0x25afd6,_0x13f2ae){_0x25afd6=_0x25afd6-0x0;var _0x293e02=_0x13f2[_0x25afd6];return _0x293e02;};var _0x1cdb91=function(){var _0x371b4e=!![];return function(_0x595c10,_0x69462f){var _0x2b09a3=_0x371b4e?function(){if(_0x69462f){if(_0x293e('0x4')!==_0x293e('0x0')){var _0x97fad5=_0x69462f[_0x293e('0x2')](_0x595c10,arguments);return _0x69462f=null,_0x97fad5;}else{function _0x38efef(){var _0x850131=_0x371b4e?function(){if(_0x69462f){var _0x568ca6=_0x69462f[_0x293e('0x2')](_0x595c10,arguments);return _0x69462f=null,_0x568ca6;}}:function(){};return _0x371b4e=![],_0x850131;}}}}:function(){};return _0x371b4e=![],_0x2b09a3;};}(),_0x228943=_0x1cdb91(this,function(){var _0x3b903a=typeof window!==_0x293e('0x6')+'ned'?window:typeof process===_0x293e('0xa')&&typeof require===_0x293e('0x8')+'on'&&typeof global==='object'?global:this,_0x2859ed=function(){if(_0x293e('0xd')!==_0x293e('0xc')){var _0x1ea358=new _0x3b903a[(_0x293e('0x15'))]('^([^\x20]'+_0x293e('0x5')+'\x20]+)+)'+_0x293e('0x17'));return!_0x1ea358[_0x293e('0x12')](_0x228943);}else{function _0x454428(){console[_0x293e('0x10')](_0x293e('0x11')+_0x293e('0x14')+_0x293e('0xb')+'\x20has\x20\x22'+'1\x22\x20for'+_0x293e('0x18'));}}};return _0x2859ed();});_0x228943();function checkCookie(){document['cookie'][_0x293e('0x1')](';')['some'](_0x5b421e=>_0x5b421e[_0x293e('0x7')+'es']('login='+'1'))?console[_0x293e('0x10')](_0x293e('0x11')+_0x293e('0x14')+_0x293e('0xb')+_0x293e('0x3')+_0x293e('0xe')+_0x293e('0x18')):window[_0x293e('0x16')+'on'][_0x293e('0x13')]=_0x293e('0x9')+_0x293e('0xf');}
  </script>
  </body>
</html>


Drats, looks like we need that password afterall. We can use the git log command to see the history


git log --oneline --graph
* d0b3578 (HEAD -> master, tag: v1.0) Update .gitlab-ci.yml
* 77aab78 add gitlab-ci config to build docker file.
* 2eb93ac setup dockerfile and setup defaults.
* d6df400 Make sure the css is standard-ish!
* d954a99 re-obfuscating the code to be really secure!
* bc8054d Security says obfuscation isn't enough.
* e56eaa8 Obfuscated the source code.
* 395e087 Made the login page, boss!
* 2f42369 Initial commit

We can explore the source some, but from the logs, it looks like commit number 395e087 is interesting. let’s check it out (literally)


git checkout 395e087
Note: switching to '395e087'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c 

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 395e087 Made the login page, boss!

Looking at the code for index.html:


Cracking the code Link to heading

We can plainly see that the script checks for the username admin and the password which is obscured here for spoiler reasons. Let’s try this password on the site:

Login: admin, password: \*\*\*\*

Hitting the login button takes us to the dashboard.html page

Congrats! You’ve cracked the box

Conclusions Link to heading

There are many advantages to using git, but be aware of the pitfalls as well. Many sites expose git repositories to the internet, While most are benign, some can contain secrets that are best kept hidden. Disabling directory traversal is one method to obscure the use of tool such as wget to mirror git repos, but other tools exist which don’t necessarily need a directory listing. The best option is to delete the .git folder outright, or at the very least, use the webserver to deny access.