Docker Registry


During the network scan with the pivoting technique, the 172.18.0.7 host was discovered with the port 5000 up and running a service Performing an additional Nmap scan

┌──(kali㉿kali)-[~/archive/htb/labs/cybermonday]
└─$ proxychains -q nmap -sC -sV -p5000 172.18.0.7
starting nmap 7.94 ( https://nmap.org ) at 2023-08-22 21:05 CEST
stats: 0:01:05 elapsed; 0 hosts completed (1 up), 1 undergoing Service Scan
service scan timing: About 0.00% done
Nmap scan report for 172.18.0.7
Host is up (0.082s latency).
 
PORT     STATE SERVICE VERSION
5000/tcp open  http    docker registry (api: 2.0)
|_http-title: Site doesn't have a title.
 
service detection performed. please report any incorrect results at https://nmap.org/submit/ .
nmap done: 1 IP address (1 host up) scanned in 95.99 seconds

The 172.18.0.7 host is running a Docker Registry service API Version 2.0

┌──(kali㉿kali)-[~/archive/htb/labs/cybermonday]
└─$ proxychains -q curl http://172.18.0.7:5000/v2/_catalog
{"repositories":["cybermonday_api"]}

The registry contains a single repository; cybermonday_api Based on the name, I’d assume that it contains a Docker image for the API web app.

┌──(kali㉿kali)-[~/archive/htb/labs/cybermonday]
└─$ proxychains -q curl http://172.18.0.7:5000/v2/_catalog -k
{"repositories":["cybermonday_api"]}

I can check if the registry requires authentication for access with the -k flag No authentication required

┌──(kali㉿kali)-[~/archive/htb/labs/cybermonday]
└─$ proxychains -q curl http://172.18.0.7:5000/v2/cybermonday_api/tags/list
{"name":"cybermonday_api","tags":["latest"]}

Listing the versions shows just one; latest

┌──(kali㉿kali)-[~/archive/htb/labs/cybermonday]
└─$ proxychains -q docker pull 172.18.0.7:5000/cybermonday_api
using default tag: latest
error response from daemon: Get "https://172.18.0.7:5000/v2/": dial tcp 172.18.0.7:5000: connect: no route to host

Attempting to pull the image with docker fails. I’d need to find a way around and there is a tool for that

dockerregistrygrabber is a Python tool to enumerate/dump docker registry (with or without basic authentication)

┌──(kali㉿kali)-[~/…/htb/labs/cybermonday/docker]
└─$ proxychains -q python3 DockerGrabber.py --help            
[+]======================================================[+]
[|]    Docker Registry Grabber v1       @SyzikSecu       [|]
[+]======================================================[+]
 
usage: DockerGrabber.py [-h] [-p port] [-U USERNAME] [-P PASSWORD]
                        [--dump DOCKERNAME | --list | --dump_all]
                        url
 
positional arguments:
  url                URL
 
options:
  -h, --help         show this help message and exit
  -p port            port to use (default : 5000)
  --dump DOCKERNAME  DockerName
  --list
  --dump_all
 
authentication:
  -U USERNAME        Username
  -P PASSWORD        Password

The tool is fairly simple to use It does also support listing repos

┌──(kali㉿kali)-[~/…/htb/labs/cybermonday/docker]
└─$ proxychains -q python3 dockergrabber.py http://172.18.0.7:5000 --list
[+]======================================================[+]
[|]    Docker Registry Grabber v1       @SyzikSecu       [|]
[+]======================================================[+]
 
[+] cybermonday_api

There is the cybermonday_api repo

┌──(kali㉿kali)-[~/…/htb/labs/cybermonday/docker]
└─$ proxychains -q python3 dockergrabber.py http://172.18.0.7 --dump_all
[+]======================================================[+]
[|]    Docker Registry Grabber v1       @SyzikSecu       [|]
[+]======================================================[+]
 
[+] cybermonday_api
[+] BlobSum found 27
[+] Dumping cybermonday_api
    [+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] downloading : beefd953abbcb2b603a98ef203b682f8c5f62af19835c01206693ad61aed63ce
    [+] downloading : ced3ae14b696846cab74bd01a27a10cb22070c74451e8c0c1f3dcb79057bcc5e
    [+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] downloading : ca62759c06e1877153b3eab0b3b734d6072dd2e6f826698bf55aedf50c0959c1
    [+] downloading : 1696d1b2f2c3c8b37ae902dfd60316f8928a31ff8a5ed0a2f9bbf255354bdee8
    [+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] downloading : 57cdb531a15a172818ddf3eea38797a2f5c4547a302b65ab663bac6fc7ec4d4f
    [+] downloading : 4756652e14e0fb6403c377eb87fd1ef557abc7864bf93bf7c25e19f91183ce2c
    [+] downloading : 5c3b6a1cbf5455e10e134d1c129041d12a8364dac18a42cf6333f8fee4762f33
    [+] downloading : 9f5fbfd5edfcaf76c951d4c46a27560120a1cd6a172bf291a7ee5c2b42afddeb
    [+] downloading : 57fbc4474c06c29a50381676075d9ee5e8dca9fee0821045d0740a5bc572ec95
    [+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] downloading : dc968f4da64f18861801f2c677d2460c4cc530f2e64232f1a23021a9760ffdae
    [+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] downloading : 1684de57270ea8328d20b9d17cda5091ec9de632dbba9622cce10b82c2b20e62
    [+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] downloading : affe9439d2a25f35605a4fe59d9de9e65ba27de2403820981b091ce366b6ce70
    [+] downloading : a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    [+] downloading : 5b5fe70539cd6989aa19f25826309f9715a9489cf1c057982d6a84c1ad8975c7
 
┌──(kali㉿kali)-[~/…/htb/labs/cybermonday/docker]
└─$ ll
total 20K
4.0k drwxr-xr-x 2 kali kali 4.0k aug 22 21:24 cybermonday_api
4.0k drwxr-xr-x 3 kali kali 4.0k aug 22 21:23 .
4.0k drwxr-xr-x 6 kali kali 4.0k aug 22 21:22 ..
8.0k -rw-r--r-- 1 kali kali 5.7k aug 22 21:22 DockerGrabber.py

Using the --dump_all flag to download all the blobs

┌──(kali㉿kali)-[~/…/htb/labs/cybermonday/docker]
└─$ ll cybermonday_api         
total 190M
 28m -rw-r--r-- 1 kali kali  28m aug 22 21:24 5b5fe70539cd6989aa19f25826309f9715a9489cf1c057982d6a84c1ad8975c7.tar.gz
4.0k drwxr-xr-x 2 kali kali 4.0k aug 22 21:24 .
4.0k -rw-r--r-- 1 kali kali   32 aug 22 21:24 a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.tar.gz
4.0k -rw-r--r-- 1 kali kali  224 aug 22 21:24 affe9439d2a25f35605a4fe59d9de9e65ba27de2403820981b091ce366b6ce70.tar.gz
100m -rw-r--r-- 1 kali kali 100m aug 22 21:24 1684de57270ea8328d20b9d17cda5091ec9de632dbba9622cce10b82c2b20e62.tar.gz
4.0k -rw-r--r-- 1 kali kali  269 aug 22 21:24 dc968f4da64f18861801f2c677d2460c4cc530f2e64232f1a23021a9760ffdae.tar.gz
 12m -rw-r--r-- 1 kali kali  12m aug 22 21:24 57fbc4474c06c29a50381676075d9ee5e8dca9fee0821045d0740a5bc572ec95.tar.gz
4.0k -rw-r--r-- 1 kali kali  492 aug 22 21:24 9f5fbfd5edfcaf76c951d4c46a27560120a1cd6a172bf291a7ee5c2b42afddeb.tar.gz
 35m -rw-r--r-- 1 kali kali  35m aug 22 21:24 5c3b6a1cbf5455e10e134d1c129041d12a8364dac18a42cf6333f8fee4762f33.tar.gz
4.0k -rw-r--r-- 1 kali kali 2.4k aug 22 21:23 4756652e14e0fb6403c377eb87fd1ef557abc7864bf93bf7c25e19f91183ce2c.tar.gz
4.0k -rw-r--r-- 1 kali kali  243 aug 22 21:23 57cdb531a15a172818ddf3eea38797a2f5c4547a302b65ab663bac6fc7ec4d4f.tar.gz
 15m -rw-r--r-- 1 kali kali  15m aug 22 21:23 1696d1b2f2c3c8b37ae902dfd60316f8928a31ff8a5ed0a2f9bbf255354bdee8.tar.gz
116k -rw-r--r-- 1 kali kali 116k aug 22 21:23 ca62759c06e1877153b3eab0b3b734d6072dd2e6f826698bf55aedf50c0959c1.tar.gz
512k -rw-r--r-- 1 kali kali 510k aug 22 21:23 ced3ae14b696846cab74bd01a27a10cb22070c74451e8c0c1f3dcb79057bcc5e.tar.gz
512k -rw-r--r-- 1 kali kali 510k aug 22 21:23 beefd953abbcb2b603a98ef203b682f8c5f62af19835c01206693ad61aed63ce.tar.gz
4.0k drwxr-xr-x 3 kali kali 4.0k aug 22 21:23 ..

The dump contains the list of blobs, and each represent a commit to the image

┌──(kali㉿kali)-[~/…/labs/cybermonday/docker/cybermonday_api]
└─$ mkdir decompressed ; cat *.tar.gz  | tar -xzf - -C decompressed -i

Extracting content to the decompressed directory

┌──(kali㉿kali)-[~/…/labs/cybermonday/docker/cybermonday_api]
└─$ ll decompressed   
total 68K
4.0k drwxr-xr-x 17 kali kali 4.0k aug 22 21:58 .
4.0k drwxr-xr-x  3 kali kali 4.0k aug 22 21:58 ..
4.0k drwxr-xr-x  2 kali kali 4.0k jul  3 07:00 tmp
4.0k drwx------  2 kali kali 4.0k jun 14 07:09 root
4.0k drwxr-xr-x 12 kali kali 4.0k jun 14 06:37 var
4.0k drwxr-xr-x 34 kali kali 4.0k jun 14 06:36 etc
   0 lrwxrwxrwx  1 kali kali    7 jun 12 02:00 bin -> usr/bin
4.0k drwxr-xr-x  2 kali kali 4.0k jun 12 02:00 dev
   0 lrwxrwxrwx  1 kali kali    7 jun 12 02:00 lib -> usr/lib
   0 lrwxrwxrwx  1 kali kali    9 jun 12 02:00 lib32 -> usr/lib32
   0 lrwxrwxrwx  1 kali kali    9 jun 12 02:00 lib64 -> usr/lib64
   0 lrwxrwxrwx  1 kali kali   10 jun 12 02:00 libx32 -> usr/libx32
4.0k drwxr-xr-x  2 kali kali 4.0k jun 12 02:00 media
4.0k drwxr-xr-x  2 kali kali 4.0k jun 12 02:00 mnt
4.0k drwxr-xr-x  2 kali kali 4.0k jun 12 02:00 opt
4.0k drwxr-xr-x  3 kali kali 4.0k jun 12 02:00 run
   0 lrwxrwxrwx  1 kali kali    8 jun 12 02:00 sbin -> usr/sbin
4.0k drwxr-xr-x  2 kali kali 4.0k jun 12 02:00 srv
4.0k drwxr-xr-x 14 kali kali 4.0k jun 12 02:00 usr
4.0k drwxr-xr-x  2 kali kali 4.0k mar  2 14:55 boot
4.0k drwxr-xr-x  2 kali kali 4.0k mar  2 14:55 home
4.0k drwxr-xr-x  2 kali kali 4.0k mar  2 14:55 proc
4.0k drwxr-xr-x  2 kali kali 4.0k mar  2 14:55 sys

The content is an entire Linux filesystem indeed

cybermonday_api


┌──(kali㉿kali)-[~/…/cybermonday/docker/cybermonday_api/decompressed]
└─$ cd var/www/html/ ; ll
total 64K
4.0K -rw-r--r-- 1 kali kali  153 Jun 30 17:26 config.php
4.0K drwxr-xr-x 6 kali kali 4.0K Jun 14 06:37 .
4.0K drwxr-xr-x 3 kali kali 4.0K Jun 14 06:37 ..
4.0K drwxr-xr-x 2 kali kali 4.0K May 29 04:03 public
4.0K -rw-r--r-- 1 kali kali   10 May 29 03:46 .dockerignore
4.0K drwxr-xr-x 2 kali kali 4.0K May 29 01:51 keys
4.0K -rw-r--r-- 1 kali kali  328 May 29 01:20 composer.json
 24K -rw-r--r-- 1 kali kali  22K May 29 01:20 composer.lock
4.0K drwxr-xr-x 9 kali kali 4.0K May 29 01:20 vendor
4.0K drwxr-xr-x 8 kali kali 4.0K May 28 19:36 app
4.0K -rw-r--r-- 1 kali kali   56 May  8 18:25 bootstrap.php

Looking at the web directory reveals a JS app, which is likely the API web application

┌──(kali㉿kali)-[~/…/decompressed/var/www/html]
└─$ cat composer.json 
{
    "autoload": {
        "psr-4": {
            "app\\": "app/"
        },
        "files": [
            "app/functions/webhook_actions.php",
            "app/functions/jwt.php"
        ]
    },
    "require": {
        "ramsey/uuid": "^4.7",
        "firebase/php-jwt": "4.0",
        "codercat/jwk-to-pem": "^1.1"
    }
}

It is the indeed the API web app

┌──(kali㉿kali)-[~/…/decompressed/var/www/html]
└─$ ll public      
total 20K
4.0K -rw-r--r-- 1 kali kali   98 Jun 30 17:25 index.php
4.0K drwxr-xr-x 6 kali kali 4.0K Jun 14 06:37 ..
4.0K -rw-r--r-- 1 kali kali  602 May 29 04:03 .htaccess
4.0K drwxr-xr-x 2 kali kali 4.0K May 29 04:03 .
4.0K -rw-r--r-- 1 kali kali  447 May 29 01:41 jwks.json

There is the secret key; jwks.json I will try to compare this with the one from the live instance

┌──(kali㉿kali)-[~/…/decompressed/var/www/html]
└─$ cat public/jwks.json
{
	"keys": [
		{
			"kty": "RSA",
			"use": "sig",
			"alg": "RS256",
			"n": "pvezvAKCOgxwsiyV6PRJfGMul-WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP_8jJ7WA2gDa8oP3N2J8zFyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn797IlIYr6Wqfc6ZPn1nsEhOrwO-qSD4Q24FVYeUxsn7pJ0oOWHPD-qtC5q3BR2M_SxBrxXh9vqcNBB3ZRRA0H0FDdV6Lp_8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhngysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh16w",
			"e": "AQAB"
		}
	]
}                                                                                                                           
┌──(kali㉿kali)-[~/…/decompressed/var/www/html]
└─$ curl http://webhooks-api-beta.cybermonday.htb/jwks.json             
{
	"keys": [
		{
			"kty": "RSA",
			"use": "sig",
			"alg": "RS256",
			"n": "pvezvAKCOgxwsiyV6PRJfGMul-WBYorwFIWudWKkGejMx3onUSlM8OA3PjmhFNCP_8jJ7WA2gDa8oP3N2J8zFyadnrt2Xe59FdcLXTPxbbfFC0aTGkDIOPZYJ8kR0cly0fiZiZbg4VLswYsh3Sn797IlIYr6Wqfc6ZPn1nsEhOrwO-qSD4Q24FVYeUxsn7pJ0oOWHPD-qtC5q3BR2M_SxBrxXh9vqcNBB3ZRRA0H0FDdV6Lp_8wJY7RB8eMREgSe48r3k7GlEcCLwbsyCyhngysgHsq6yJYM82BL7V8Qln42yij1BM7fCu19M1EZwR5eJ2Hg31ZsK5uShbITbRh16w",
			"e": "AQAB"
		}
	]
}

They are identical. This confirms it

Keys


┌──(kali㉿kali)-[~/…/var/www/html/keys]
└─$ ll
total 16K
4.0k -rw-r--r-- 1 kali kali  451 jun 30 17:48 public.pem
4.0k -rw------- 1 kali kali 1.7k jun 30 17:48 private.pem

There is also the public/private key pair for the web application

Router.php


The app/routes/Router.php file provides a simple configuration for routing different HTTP requests All the API endpoints are listed here, and their co-responding controller actions to handle the request appropriately

IndexController.php


The app/controllers/IndexController.php file also provides somewhat similar information to the Router.php file But it also does provide the available routes, their methods, and potential parameters in those API enpoints

AuthController.php


The app/controllers/AuthController.php file appears to handle both register and login actions

  • The register action takes both username and passsword parameters
    • the role parameter is predefined to user
      • meaning that it’s impossible to become admin via the register action
  • The login action generates a token(x-access-token) that contains 3 attributes; id, username, and role

jwt.php


The app/functions/jwt.php file is used by the app/controllers/AuthController.php file above to handle the login action with the createToken function It uses the public/private key pair above

LogsController.php


<?php
 
namespace app\controllers;
use app\helpers\Api;
use app\models\Webhook;
 
class LogsController extends Api
{
    public function index($request)
    {
        $this->apiKeyAuth();
 
        $webhook = new Webhook;
        $webhook_find = $webhook->find("uuid", $request->uuid);
 
        if(!$webhook_find)
        {
            return $this->response(["status" => "error", "message" => "Webhook not found"], 404);
        }
 
        if($webhook_find->action != "createLogFile")
        {
            return $this->response(["status" => "error", "message" => "This webhook was not created to manage logs"], 400);
        }
 
        $actions = ["list", "read"];
 
        if(!isset($this->data->action) || empty($this->data->action))
        {
            return $this->response(["status" => "error", "message" => "\"action\" not defined"], 400);
        }
 
        if($this->data->action == "read")
        {
            if(!isset($this->data->log_name) || empty($this->data->log_name))
            {
                return $this->response(["status" => "error", "message" => "\"log_name\" not defined"], 400);
            }
        }
 
        if(!in_array($this->data->action, $actions))
        {
            return $this->response(["status" => "error", "message" => "invalid action"], 400);
        }
 
        $logPath = "/logs/{$webhook_find->name}/";
 
        switch($this->data->action)
        {
            case "list":
                $logs = scandir($logPath);
                array_splice($logs, 0, 1); array_splice($logs, 0, 1);
 
                return $this->response(["status" => "success", "message" => $logs]);
            
            case "read":
                $logName = $this->data->log_name;
 
                if(preg_match("/\.\.\//", $logName))
                {
                    return $this->response(["status" => "error", "message" => "This log does not exist"]);
                }
 
                $logName = str_replace(' ', '', $logName);
 
                if(stripos($logName, "log") === false)
                {
                    return $this->response(["status" => "error", "message" => "This log does not exist"]);
                }
 
                if(!file_exists($logPath.$logName))
                {
                    return $this->response(["status" => "error", "message" => "This log does not exist"]);
                }
 
                $logContent = file_get_contents($logPath.$logName);
                
 
 
                return $this->response(["status" => "success", "message" => $logContent]);
        }
    }
}

Surprisingly, the app/controllers/LogsController.php file defines 2 additional actions that was not shown in the app/controllers/IndexController.php file above, but this ONLY gets invoked from the API Endpoint at /webhooks/:uuid/logs Those are list and read actions;

  • list Action:

    • The list action is triggered if it’s supplied to the action parameter of the API Endpoint at /webhooks/:uuid/logs
    • This action aims to retrieve a list of log files associated with a particular webhook
    • It uses the scandir function to list the files in a specified directory (log directory associated with the webhook)
    • The initial elements . and .. (current directory and parent directory) are removed from the list
    • The resulting list of log files is returned as a response with a “success” status
  • read Action:

    • The list action is triggered if it’s supplied to the action parameter of the API Endpoint at /webhooks/:uuid/logs
    • This action is designed to read the content of a specific log file associated with a webhook
    • It first checks if the requested log file exists in the specified log directory
    • If the log file doesn’t exist or if the log name includes certain forbidden characters or patterns, an appropriate error response is returned
    • If the log file exists and passes the checks, the content of the log file is read using file_get_contents
    • The content of the log file is then returned as a response with a “success” status

I believe that these 2 actions could be abused for file read

Another important point to make here is the following line; $this->apiKeyAuth();

  • appears to be a prerequisite to get to either of actions above
  • performs a form authentication process by calling the apiKeyAuth() function
    • if the apiKeyAuth() function does not return true, it’s impossible to get to either of actions above
  • The apiKeyAuth() function is present in the app\helpers\Api.php file below

Api.php


<?php
 
namespace app\helpers;
use app\helpers\Request;
 
abstract class Api
{
    protected $data;
    protected $user;
    private $api_key;
 
    public function __construct()
    {
        $method = request::method();
        if(!isset($_SERVER['CONTENT_TYPE']) && $method != "get" || $method != "get" && $_SERVER['CONTENT_TYPE'] != "application/json")
        {
            return http_response_code(404);
        }
 
        header('content-type: application/json; charset=utf-8');
        $this->data = json_decode(file_get_contents("php://input"));
    }
 
    public function auth()
    {
        if(!isset($_SERVER["HTTP_X_ACCESS_TOKEN"]) || empty($_SERVER["HTTP_X_ACCESS_TOKEN"]))
        {
            return $this->response(["status" => "error", "message" => "Unauthorized"], 403);
        }
 
        $token = $_SERVER["HTTP_X_ACCESS_TOKEN"];
        $decoded = decodeToken($token);
        if(!$decoded)
        {
            return $this->response(["status" => "error", "message" => "Unauthorized"], 403);
        }
    
        $this->user = $decoded;
    }
 
    public function apiKeyAuth()
    {
        $this->api_key = "22892e36-1770-11ee-be56-0242ac120002";
 
        if(!isset($_SERVER["HTTP_X_API_KEY"]) || empty($_SERVER["HTTP_X_API_KEY"]) || $_SERVER["HTTP_X_API_KEY"] != $this->api_key)
        {
            return $this->response(["status" => "error", "message" => "Unauthorized"], 403);
        }
    }
 
    public function admin()
    {
        $this->auth();
        
        if($this->user->role != "admin")
        {
            return $this->response(["status" => "error", "message" => "Unauthorized"], 403);
        }
    }
 
    public function response(array $data, $status = 200) {
        http_response_code($status);
        die(json_encode($data));
    }

The app\helpers\Api.php file indeed contains the apiKeyAuth() function mentioned above It seems to check for the api_key variable in the X_API_KEY header

  • The api_key variable is essentially a secret key;22892e36-1770-11ee-be56-0242ac120002

x_api_key: 22892e36-1770-11ee-be56-0242ac120002 So this means that I just need to supply the api_key variable in the X_API_KEY header when attempting to abuse those 2 actions above