Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions docs/_docs/user-guide/tomes.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,189 @@ On GitHub, you can easily accomplish this by adding Tavern's public key for the
Now, all that's left is to click "Import tomes". If all goes well, your [Tomes](/user-guide/terminology#tome) will be added to Tavern and will be displayed on the view. If [Tomes](/user-guide/terminology#tome) are missing, be sure each [Tome](/user-guide/terminology#tome) has a valid `metadata.yml` and `main.eldritch` file in the [Tome](/user-guide/terminology#tome) root directory.

Anytime you need to re-import your [Tomes](/user-guide/terminology#tome) (for example, after an update), you may navigate to the "Tomes" page and click "Refetch tomes".

## Tome writing best practices

Writing tomes will always be specific to your use case. Different teams, different projects will prioritize different things. In this guide we'll prioritize reliability and safety at the expense of OPSEC.

OPSEC considerations will tend towards avoiding calls to `shell` and `exec` instead using native functions to accomplish the function.

In some situations you may also wish to avoid testing on target if that's the case you should test throughly off target before launching.

If you test off target you can leverage native functions like [`sys.get_os`](/user-guide/eldritch#sysget_os) to ensure that your tome only runs against targets it's been tested on.

### Example

Copy and run an asset in a safe and idempotant way

```python
IMPLANT_ASSET_PATH = "tome_name/assets/implant"
ASSET_SHA1SUM = "44e1bf82832580c11de07f57254bd4af837b658e"

def pre_flight(dest_bin_path):
## Testing

# Note that we're using multiple returns instead of
# nesting if statements. This style is to ensule
# line of sight readability when reviewing code.
if not sys.is_linux():
print("[error] tome only supports linux")
return False

cur_user = sys.get_user()
if cur_user['euid']['uid'] != 0:
print(f"[error] tome must be run as root / uid 0 not {cur_user}")
return False

# Validate the destination directory exists
if not file.is_dir(file.parent_dir(dest_bin_path)):
print(f"[error] path {dest_bin_path} parent isn't a directory")
return False

if file.is_file(dest_bin_path):
print(f"[error] path {dest_bin_path} already exists")
return False

does_chmod_exist = sys.shell(f"command -v chmod")
if does_chmod_exist['status'] != 0:
print(f"[error] tome requires `chmod` be available in PATH")
return False

return True


def deploy_asset(asset_path, dest, asset_hash):
if file.is_file(asset_path):
if crypto.hash_file(asset_path, "SHA1") != asset_hash:
print(f"{dest} file already exists and is good")
return True

pdir = file.parent_dir(dest)
if not file.is_dir(pdir):
print(f"{pdir} isn't a dir aborting")
return False

asset.copy(asset_path, dest)
return True

def set_perms(dest, perms):
cur_perms = sys.shell(f"stat -f %A {dest}")
if cur_perms['status'] == 0:
if cur_perms['stdout'].strip() == perms:
print(f"dest perms already set")
return True

res = sys.shell(f"chmod {perms} {dest}")
print(f"modified perms of {dest}")
pprint(res)
if res['status'] == 0
return True

print(f"failed to set perms {perms} on {dest}")
return False

def execute_once(dest):
for p in process.list():
if p['path'] == dest
print(f"process {dest} is already running")
pprint(p)
return True

res = sys.exec(dest, [], True)

for p in process.list():
if p['path'] == dest
print(f"process {dest} is now running")
pprint(p)
return True

return False

def cleanup(dest):
if file.is_file(dest):
print(f"cleaning up {dest}")
file.remove(dest)


def main(dest_file_path):
if not pre_flight():
cleanup(dest_file_path)
return
if not deploy_asset(ASSET_PATH, dest_file_path, ASSET_SHA1SUM):
cleanup(dest_file_path)
return
if not set_perms(dest_file_path, "755"):
cleanup(dest_file_path)
return
execute_once(dest_file_path)


main(input_params['DEST_FILE_PATH'])
```

### Passing input
Tomes have an inherent global variable `input_params` that defines variables passed from the UI.
These are defined in your `metadata.yml` file.
Best practice is to pass these into your `main` function and sub functions to keep them reusable.
There is currently no way to define `input_params` during golem run time so if you're using golem to test tomes you may need to manually define them. Eg.

```python
input_params = {}
input_params['DEST_FILE_PATH'] = "/bin/imix"

main(input_params['DEST_FILE_PATH'])
```

### Fail early
Before modifying anything on Target or loading assets validate that the tome you're building will run as expected.
This avoids artifacts being left on disk if something fails and also provides verbose feedback in the event of an error to reduce trouble shooting time.

Common things to validate:
- Operating systems - all tomes are cross platform so it's up to the tome developer to fail if run on an unsupported OS
- Check parent directory exists using [`fileparent_dir`](/user-guide/eldritch#fileparent_dir)
- User permissions
- Required dependencies or LOLBINs

**Note on `eprint`: eprint has bugs causing tomes to exit prematurely. Currently best practice is to avoid using it.**

### Backup and Test
If you need to modify configuration that might cause breaking changes to the system.
It's recommended that you create a backup, modify the config, and validate your change.
If something fails during validation replace the original config.

This might look like:
```python
file.copy(config_path, f"{config_path}.bak")

if not end_to_end_test():
print("[error] end to end test failed cleaning up")
file.remove(config_path)
file.moveto(f"{config_path}.bak", config_path)
return

file.remove(f"{config_path}.bak")
```

End to end test can validate the backdoor - if possible validate normal system functionality.

```python
# Auth backdoor - drop to a user account or `nobody` and then auth to a user account
sys.shell(f"echo -e '{password}\n{password}\n' | su {user} -c 'su {user} -c id'")

# Bind shell - check against localhost
res = sys.shell(f"{shell_client} 127.0.0.1 id")
```


### Idempotence
In many situations especially when deploying persistance you'll want to ensure that running a tome twice won't cause issues.
The best way to handle this is to when possible check the current state of the resource you're about to modify.

This often takes the shape of running validation before and after taking an action.
In the same way during manual operation you might check if a file is on disk and the right size you can automate that with eldritch.

Things to keep in mind:
- Does the file hash match?
- Do the permissions match?
- Is the process already running?
- Is the configuration already set?
Loading